/**
 * Services Manager for Onebox
 *
 * Orchestrates service deployment: Docker + Nginx + DNS + SSL
 */

import type { ICertRequirement, IService, IServiceDeployOptions, IPlatformRequirements, TStorageClassPurpose } from '../types.js';
import * as plugins from '../plugins.js';
import { logger } from '../logging.js';
import { getErrorMessage } from '../utils/error.js';
import { OneboxDatabase } from '../database/index.js';
import { OneboxDockerManager } from './docker.js';
import type { PlatformServicesManager } from './platform-services/index.js';

export type TServiceUpdateProgressStep =
  | 'stopping'
  | 'pulling-image'
  | 'updating-record'
  | 'removing-container'
  | 'creating-container'
  | 'starting'
  | 'restoring-route'
  | 'syncing-gateway'
  | 'complete';

export interface IServiceUpdateProgress {
  step: TServiceUpdateProgressStep;
  message: string;
}

export interface IServiceUpdateOptions {
  onProgress?: (progressArg: IServiceUpdateProgress) => void | Promise<void>;
}

interface IDeploymentSideEffects {
  localRouteDomains: Set<string>;
  dnsRecordDomains: Set<string>;
  externalGatewaySyncAttempted: boolean;
}

export class OneboxServicesManager {
  private oneboxRef: any; // Will be Onebox instance
  private database: OneboxDatabase;
  private docker: OneboxDockerManager;
  private autoUpdateIntervalId: ReturnType<typeof setInterval> | null = null;

  constructor(oneboxRef: any) {
    this.oneboxRef = oneboxRef;
    this.database = oneboxRef.database;
    this.docker = oneboxRef.docker;
  }

  private async broadcastServiceUpdate(
    serviceName: string,
    action: 'created' | 'updated' | 'deleted' | 'started' | 'stopped',
  ): Promise<void> {
    await this.oneboxRef.opsServer.broadcastServiceUpdate(
      serviceName,
      action,
      await this.database.getServiceByName(serviceName),
    );
  }

  private async syncExternalGatewayRoute(service: IService, optionsArg: { strict?: boolean } = {}): Promise<void> {
    if (!this.oneboxRef.externalGateway) return;
    try {
      await this.oneboxRef.externalGateway.syncServiceRoute(service);
    } catch (error) {
      if (optionsArg.strict) throw error;
      logger.warn(`Failed to sync external gateway route for ${service.name}: ${getErrorMessage(error)}`);
    }
  }

  private async deleteExternalGatewayPublicRoutesStrict(
    service: Pick<IService, 'id' | 'name' | 'publicPortMappings'>,
  ): Promise<void> {
    if (!this.oneboxRef.externalGateway) return;
    await this.oneboxRef.externalGateway.deleteServicePublicRoutes(service);
  }

  private async deleteExternalGatewayRouteStrict(service: Pick<IService, 'id' | 'name' | 'domain'>): Promise<void> {
    if (!this.oneboxRef.externalGateway) return;
    await this.oneboxRef.externalGateway.deleteServiceRoute(service);
  }

  private async deleteExternalGatewayRoutesStrict(
    service: Pick<IService, 'id' | 'name' | 'domain' | 'domains' | 'targetPorts' | 'publicPortMappings' | 'port'>,
  ): Promise<void> {
    if (!this.oneboxRef.externalGateway) return;
    for (const domain of this.getServiceDomainNames(service)) {
      await this.oneboxRef.externalGateway.deleteServiceRoute({ ...service, domain });
    }
  }

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

  private async isImageReferencedByOtherService(serviceArg: IService, imageNameArg: string): Promise<boolean> {
    const services = await this.database.getAllServices();
    return services.some((candidateArg) =>
      candidateArg.id !== serviceArg.id && this.getRuntimeImageName(candidateArg) === imageNameArg
    );
  }

  private async isDomainUsedByOtherService(serviceArg: IService, domainArg: string): Promise<boolean> {
    const services = await this.database.getAllServices();
    return services.some((candidateArg) =>
      candidateArg.id !== serviceArg.id && this.getServiceDomainNames(candidateArg).includes(domainArg)
    );
  }

  private async removeDnsRecordIfServiceOwned(serviceArg: IService, domainArg: string): Promise<void> {
    if (await this.isDomainUsedByOtherService(serviceArg, domainArg)) return;
    const dnsRecord = await this.database.getDnsRecordByDomain(domainArg);
    if (!dnsRecord) return;
    await this.oneboxRef.dns.removeDNSRecord(domainArg);
  }

  private async cleanupUnreferencedCertificates(requirementsArg: ICertRequirement[]): Promise<void> {
    const certificateIds = new Set(
      requirementsArg
        .map((requirementArg) => requirementArg.certificateId)
        .filter((certificateIdArg): certificateIdArg is number => typeof certificateIdArg === 'number'),
    );
    if (certificateIds.size === 0) return;

    const remainingRequirements = await this.database.getAllCertRequirements();
    for (const certificateId of certificateIds) {
      const stillReferenced = remainingRequirements.some((requirementArg) =>
        requirementArg.certificateId === certificateId
      );
      if (!stillReferenced) {
        await this.database.deleteCertificate(certificateId);
      }
    }
  }

  private async cleanupOneboxRegistryImage(serviceArg: IService): Promise<void> {
    if (!serviceArg.useOneboxRegistry || !serviceArg.registryRepository) return;
    const tag = serviceArg.registryImageTag || 'latest';
    const services = await this.database.getAllServices();
    const referencedByOtherService = services.some((candidateArg) =>
      candidateArg.id !== serviceArg.id &&
      candidateArg.useOneboxRegistry &&
      candidateArg.registryRepository === serviceArg.registryRepository &&
      (candidateArg.registryImageTag || 'latest') === tag
    );
    if (referencedByOtherService) return;
    await this.oneboxRef.registry.deleteImage(serviceArg.registryRepository, tag);
  }

  private async resolveStorageVolumes(
    volumesArg: IService['volumes'] = [],
    purposeArg: TStorageClassPurpose = 'runtime',
  ): Promise<IService['volumes']> {
    return await Promise.all(
      volumesArg.map((volumeArg) => this.database.resolveServiceVolumeStorage(volumeArg, purposeArg)),
    );
  }

  private normalizeServicePortConfig(
    serviceArg: Pick<IService, 'name' | 'domain' | 'targetPorts' | 'domains' | 'publicPortMappings' | 'port'>,
  ): plugins.servezoneInterfaces.data.INormalizedServicePortConfig {
    return plugins.servezoneInterfaces.data.normalizeServicePortConfig({
      name: serviceArg.name,
      targetPorts: serviceArg.targetPorts,
      domains: serviceArg.domains?.length
        ? serviceArg.domains
        : serviceArg.domain
          ? [{ name: serviceArg.domain, protocol: 'https' }]
          : [],
      publicPortMappings: serviceArg.publicPortMappings || [],
    }, { allowLegacyDomainPort: false });
  }

  private resolveServiceRoutePort(
    serviceArg: Pick<IService, 'name' | 'domain' | 'domains' | 'targetPorts' | 'publicPortMappings' | 'port'>,
    domainArg?: string,
  ): number {
    const normalizedPortConfig = this.normalizeServicePortConfig(serviceArg);
    const normalizedDomain = domainArg?.trim().toLowerCase();
    const domainRoute = normalizedDomain
      ? normalizedPortConfig.domains.find((domainEntryArg) => domainEntryArg.name.trim().toLowerCase() === normalizedDomain)
      : normalizedPortConfig.domains[0];
    const routePort = domainRoute?.target.port || normalizedPortConfig.defaultTargetPort?.port;
    if (!routePort) {
      throw new Error(`Service ${serviceArg.name} has no declared target port`);
    }
    return routePort;
  }

  private getServiceDomainNames(
    serviceArg: Pick<IService, 'name' | 'domain' | 'domains' | 'targetPorts' | 'publicPortMappings' | 'port'>,
  ): string[] {
    return this.normalizeServicePortConfig(serviceArg).domains.map((domainEntryArg) => domainEntryArg.name);
  }

  private hasExternalGatewayRoutes(
    serviceArg: Pick<IService, 'name' | 'domain' | 'domains' | 'targetPorts' | 'publicPortMappings' | 'port'>,
  ): boolean {
    return this.getServiceDomainNames(serviceArg).length > 0 || Boolean(serviceArg.publicPortMappings?.length);
  }

  private publicRouteRef(mappingNameArg: string, protocolArg: 'tcp' | 'udp', publicPortArg: number): string {
    return `public:${mappingNameArg}:${protocolArg}:${publicPortArg}`;
  }

  private getServicePublicRouteRefs(
    serviceArg: Pick<IService, 'name' | 'domain' | 'domains' | 'targetPorts' | 'publicPortMappings' | 'port'>,
  ): Set<string> {
    const refs = new Set<string>();
    for (const mapping of this.normalizeServicePortConfig(serviceArg).publicPortMappings) {
      for (let port = mapping.publicPort; port <= mapping.publicPortEnd; port++) {
        refs.add(this.publicRouteRef(mapping.name, mapping.protocol, port));
      }
    }
    return refs;
  }

  private getStalePublicPortMappings(
    previousServiceArg: IService,
    nextServiceArg: IService,
  ): NonNullable<IService['publicPortMappings']> {
    const nextRefs = this.getServicePublicRouteRefs(nextServiceArg);
    const staleMappings: NonNullable<IService['publicPortMappings']> = [];
    for (const mapping of this.normalizeServicePortConfig(previousServiceArg).publicPortMappings) {
      for (let port = mapping.publicPort; port <= mapping.publicPortEnd; port++) {
        if (nextRefs.has(this.publicRouteRef(mapping.name, mapping.protocol, port))) continue;
        staleMappings.push({
          name: mapping.name,
          publicPort: port,
          targetPort: mapping.targetPort,
          protocol: mapping.protocol,
          sourceProfileRef: mapping.sourceProfileRef,
          exclusive: true,
        });
      }
    }
    return staleMappings;
  }

  private async preflightPublicPortRoutes(candidateArg: IService): Promise<void> {
    if (!candidateArg.publicPortMappings?.length) return;
    const services = (await this.database.getAllServices())
      .filter((serviceArg) => serviceArg.id !== candidateArg.id && serviceArg.status === 'running' && serviceArg.containerID);
    await this.oneboxRef.reverseProxy.validatePublicPortRoutesForServices?.([
      ...services,
      {
        ...candidateArg,
        status: 'running',
        containerID: candidateArg.containerID || '__pending__',
      },
    ]);
  }

  private async rollbackPulledImage(imageCleanupArg: NonNullable<IService['imageCleanup']>): Promise<void> {
    if (imageCleanupArg.priorReference) {
      await this.docker.tagImage(imageCleanupArg.priorReference, imageCleanupArg.imageName);
      await this.docker.removeImageIfUnused(imageCleanupArg.candidateReference);
      return;
    }
    await this.docker.removeImageIfUnused(imageCleanupArg.candidateReference);
  }

  private async recoverPendingImageCleanup(serviceArg: IService): Promise<void> {
    const imageCleanup = serviceArg.imageCleanup;
    if (!imageCleanup) return;

    if (imageCleanup.phase === 'candidate-active') {
      await this.database.clearServiceImageCleanup(serviceArg.id!);
      return;
    }

    const runtimeReferences = await this.docker.collectServiceRuntimeReferences?.(serviceArg);
    const activeImageReference = runtimeReferences?.imageId || runtimeReferences?.imageReference;
    if (imageCleanup.phase === undefined && activeImageReference === imageCleanup.candidateReference) {
      await this.database.clearServiceImageCleanup(serviceArg.id!);
      return;
    }

    const uncommittedCandidateIsActive = imageCleanup.phase === 'rollback-pending'
      && activeImageReference === imageCleanup.candidateReference;
    const priorRuntimeIsMissing = imageCleanup.phase === 'rollback-pending'
      && runtimeReferences !== undefined
      && runtimeReferences.containerIds.length === 0
      && activeImageReference === undefined;
    if (uncommittedCandidateIsActive || priorRuntimeIsMissing) {
      if (uncommittedCandidateIsActive) {
        await this.docker.removeServiceRuntimeForReplacement(serviceArg);
      }
      await this.database.replaceService(serviceArg.id!, {
        ...serviceArg,
        containerID: undefined,
        status: serviceArg.status === 'starting' ? 'running' : serviceArg.status,
        imageCleanup,
      });
    }

    await this.rollbackPulledImage(imageCleanup);
    await this.database.clearServiceImageCleanup(serviceArg.id!);
  }

  private createHostedAppControlToken(): string {
    const bytes = new Uint8Array(32);
    crypto.getRandomValues(bytes);
    return Array.from(bytes).map((byteArg) => byteArg.toString(16).padStart(2, '0')).join('');
  }

  private createHostedAppRuntimeEnvVars(): {
    appInstanceId: string;
    appControlToken: string;
    envVars: Record<string, string>;
  } {
    const appInstanceId = crypto.randomUUID();
    const appControlToken = this.createHostedAppControlToken();
    const runtimeUrl =
      process.env.SERVEZONE_RUNTIME_URL ||
      process.env.ONEBOX_RUNTIME_URL ||
      'http://host.docker.internal:3000';
    return {
      appInstanceId,
      appControlToken,
      envVars: {
        SERVEZONE_RUNTIME_URL: runtimeUrl,
        SERVEZONE_APP_INSTANCE_ID: appInstanceId,
        SERVEZONE_APP_HOST_TYPE: 'onebox',
      },
    };
  }

  private async rollbackFailedDeployment(
    serviceArg: IService,
    createdRuntimeIDArg?: string,
    sideEffectsArg: IDeploymentSideEffects = {
      localRouteDomains: new Set(),
      dnsRecordDomains: new Set(),
      externalGatewaySyncAttempted: false,
    },
    imageCleanupArg?: IService['imageCleanup'],
  ): Promise<void> {
    const currentService = await this.database.getServiceByID(serviceArg.id!);
    const cleanupService: IService = {
      ...(currentService?.toData() || serviceArg),
      containerID: createdRuntimeIDArg || currentService?.containerID,
    };
    const certRequirements = await this.database.getCertRequirementsByService(serviceArg.id!);
    const failures: string[] = [];
    let runtimeMayRemain = true;
    let runtimeReferences: { volumeNames: string[] } | undefined;
    const attempt = async (labelArg: string, cleanupArg: () => Promise<unknown>) => {
      try {
        await cleanupArg();
      } catch (error) {
        failures.push(`${labelArg}: ${getErrorMessage(error)}`);
      }
    };

    if (sideEffectsArg.externalGatewaySyncAttempted) {
      await attempt('external gateway routes', () => this.deleteExternalGatewayRoutesStrict(cleanupService));
      await attempt('external gateway public routes', () => this.deleteExternalGatewayPublicRoutesStrict(cleanupService));
    }
    await attempt('public port routes', async () => {
      await this.oneboxRef.reverseProxy.reloadPublicPortRoutes?.({ excludeServiceId: serviceArg.id! });
    });
    await attempt('Docker runtime', async () => {
      runtimeReferences = await this.docker.removeServiceRuntime(cleanupService);
      runtimeMayRemain = false;
    });

    for (const domain of sideEffectsArg.localRouteDomains) {
      await attempt(`local route ${domain}`, async () => {
        await this.oneboxRef.reverseProxy.removeRoute?.(domain);
      });
    }
    for (const domain of sideEffectsArg.dnsRecordDomains) {
      await attempt(`DNS record ${domain}`, () => this.removeDnsRecordIfServiceOwned(cleanupService, domain));
    }
    if (!runtimeMayRemain) {
      await attempt('platform resources', () => (
        this.oneboxRef.platformServices as PlatformServicesManager
      ).cleanupForService(serviceArg.id!));
      if (runtimeReferences && this.docker.removeServiceOwnedVolumes) {
        await attempt('service-owned volumes', () => (
          this.docker.removeServiceOwnedVolumes(cleanupService, runtimeReferences!.volumeNames)
        ));
      }
      if (imageCleanupArg) {
        await attempt('unused pulled image', () => this.rollbackPulledImage(imageCleanupArg));
      }
    }
    await attempt('certificate requirements', () => this.database.deleteCertRequirementsByService(serviceArg.id!));
    await attempt('unreferenced certificates', () => this.cleanupUnreferencedCertificates(certRequirements));

    if (failures.length === 0) {
      await attempt('service metadata', () => this.database.deleteService(serviceArg.id!));
    }
    if (failures.length > 0) {
      await attempt('failed runtime ownership marker', () => this.database.markServiceRuntimeCleanupRequired(
        serviceArg.id!,
        runtimeMayRemain ? cleanupService.containerID : undefined,
      ));
      throw new AggregateError(failures, `Failed to fully roll back service ${serviceArg.name}`);
    }
  }

  /**
   * Deploy a new service (full workflow)
   */
  async deployService(options: IServiceDeployOptions): Promise<IService> {
    let createdService: IService | null = null;
    let createdRuntimeID: string | undefined;
    let deploymentCommitted = false;
    let imageCleanup: IService['imageCleanup'];
    const deploymentSideEffects: IDeploymentSideEffects = {
      localRouteDomains: new Set(),
      dnsRecordDomains: new Set(),
      externalGatewaySyncAttempted: false,
    };
    try {
      logger.info(`Deploying service: ${options.name}`);

      // Check if service already exists
      const existing = await this.database.getServiceByName(options.name);
      if (existing) {
        throw new Error(`Service already exists: ${options.name}`);
      }

      // Handle Onebox Registry setup
      let imageToPull: string;

      if (options.useOneboxRegistry) {
        // Use onebox registry image name
        const tag = options.registryImageTag || 'latest';
        imageToPull = this.oneboxRef.registry.getImageName(options.name, tag);
      } else {
        // Use external image
        imageToPull = options.image;
      }

      // Build platform requirements
      const platformRequirements: IPlatformRequirements | undefined =
        (options.enableMongoDB || options.enableS3 || options.enableClickHouse || options.enableValkey || options.enableMariaDB)
          ? {
              mongodb: options.enableMongoDB,
              s3: options.enableS3,
              clickhouse: options.enableClickHouse,
              valkey: options.enableValkey,
              mariadb: options.enableMariaDB,
            }
          : undefined;

      const hostedAppRuntime = this.createHostedAppRuntimeEnvVars();
      const requestedEnvVars = {
        ...(options.envVars || {}),
        ...hostedAppRuntime.envVars,
      };
      const requestedSecretEnvVars = this.database.serviceSecrets.validate({
        ...(options.secretEnvVars || {}),
        SERVEZONE_APP_CONTROL_TOKEN: hostedAppRuntime.appControlToken,
      });
      const targetPorts = options.targetPorts || [{ name: 'web', port: options.port, protocol: 'http', default: true }];
      const domains = options.domains || (options.domain ? [{ name: options.domain, protocol: 'https' }] : []);
      const publicPortMappings = options.publicPortMappings || [];

      const preflightService: IService = {
        name: options.name,
        image: options.useOneboxRegistry ? imageToPull : options.image,
        registry: options.registry,
        envVars: requestedEnvVars,
        containerArgs: options.containerArgs,
        secretFiles: options.secretFiles,
        volumes: options.volumes,
        publishedPorts: options.publishedPorts || [],
        targetPorts,
        domains,
        publicPortMappings,
        port: options.port,
        domain: options.domain,
        status: 'running',
        createdAt: Date.now(),
        updatedAt: Date.now(),
      };
      this.docker.validateServiceSpec(preflightService);
      await this.docker.assertSecretFilesSupported(options.secretFiles);
      await this.preflightPublicPortRoutes(preflightService);

      const resolvedVolumes = await this.resolveStorageVolumes(options.volumes, 'runtime');

      // Create service record in database
      const service = await this.database.createService({
        name: options.name,
        image: options.useOneboxRegistry ? imageToPull : options.image,
        registry: options.registry,
        envVars: requestedEnvVars,
        containerArgs: options.containerArgs,
        secretFiles: options.secretFiles,
        volumes: resolvedVolumes,
        publishedPorts: options.publishedPorts || [],
        targetPorts,
        domains,
        publicPortMappings,
        port: options.port,
        domain: options.domain,
        status: 'stopped',
        createdAt: Date.now(),
        updatedAt: Date.now(),
        // Onebox Registry fields
        useOneboxRegistry: options.useOneboxRegistry,
        registryRepository: options.useOneboxRegistry ? options.name : undefined,
        registryImageTag: options.registryImageTag || 'latest',
        autoUpdateOnPush: options.autoUpdateOnPush,
        imageDigest: options.imageDigest,
        // Platform requirements
        platformRequirements,
        // App Store template tracking
        appTemplateId: options.appTemplateId,
        appTemplateVersion: options.appTemplateVersion,
      });
      createdService = service.toData();

      await this.database.upsertHostedAppLifecycle({
        serviceId: service.id!,
        appInstanceId: hostedAppRuntime.appInstanceId,
        hostType: 'onebox',
        state: {
          appInstanceId: hostedAppRuntime.appInstanceId,
          hostType: 'onebox',
          appName: options.name,
          publicUrl: options.domain ? `https://${options.domain}` : undefined,
          runtimeStatus: 'unknown',
        },
      });

      // Provision platform resources if needed
      let platformEnvVars: Record<string, string> = {};
      if (platformRequirements) {
        try {
          logger.info(`Provisioning platform resources for service '${options.name}'...`);
          const platformServices = this.oneboxRef.platformServices as PlatformServicesManager;
          platformEnvVars = await platformServices.provisionForService(service);
          logger.success(`Platform resources provisioned for service '${options.name}'`);
        } catch (error) {
          logger.error(`Failed to provision platform resources: ${getErrorMessage(error)}`);
          throw error;
        }
      }

      const serviceSecrets = {
        ...requestedSecretEnvVars,
        ...platformEnvVars,
      };
      await this.database.serviceSecrets.set(service.id!, serviceSecrets);

      const serviceWithEnvVars = (await this.database.getServiceByName(options.name))!;
      this.docker.validateServiceSecrets(serviceWithEnvVars, serviceSecrets);

      // Pull image (skip if using onebox registry - image might not exist yet)
      if (!options.useOneboxRegistry) {
        const priorImageReference = await this.docker.getImageReference?.(imageToPull, options.registry);
        await this.docker.pullImage(imageToPull, options.registry);
        const candidateImageReference = await this.docker.getImageReference?.(imageToPull, options.registry);
        if (candidateImageReference && candidateImageReference !== priorImageReference) {
          imageCleanup = {
            imageName: options.registry ? `${options.registry}/${imageToPull}` : imageToPull,
            candidateReference: candidateImageReference,
            priorReference: priorImageReference,
            phase: 'rollback-pending',
          };
          await this.database.updateService(service.id!, { imageCleanup });
        }
      }

      // Create container (uses the updated service with merged env vars)
      const containerID = await this.docker.createContainer(serviceWithEnvVars, serviceSecrets);
      createdRuntimeID = containerID;

      // Update service with container ID
      await this.database.updateService(service.id!, {
        containerID,
        status: 'starting',
        ...(imageCleanup
          ? { imageCleanup: { ...imageCleanup, phase: 'candidate-active' as const } }
          : {}),
      });

      // Start container
      await this.docker.startContainer(containerID);

      // Update status
      await this.database.updateService(service.id!, { status: 'running' });
      await this.oneboxRef.opsServer?.telemetryHandler?.refreshServiceTelemetry?.(options.name);

      const serviceForRoutes = (await this.database.getServiceByName(options.name))!;
      for (const domain of this.getServiceDomainNames(serviceForRoutes)) {
        logger.info(`Configuring domain: ${domain}`);

        // Validate domain and create CertRequirement
        try {
          // Extract base domain (e.g., "api.example.com" -> "example.com")
          const domainParts = domain.split('.');
          const baseDomain = domainParts.slice(-2).join('.');
          const subdomain = domainParts.length > 2 ? domainParts.slice(0, -2).join('.') : '';

          // Check if base domain exists in Domain table
          const domainRecord = await this.database.getDomainByName(baseDomain);

          if (!domainRecord) {
            logger.warn(
              `Domain ${baseDomain} not found in Domain table. ` +
              `Service will deploy but certificate management may not work. ` +
              `Run Cloudflare domain sync or manually add the domain.`
            );
          } else if (domainRecord.isObsolete) {
            logger.warn(
              `Domain ${baseDomain} is marked as obsolete. ` +
              `Certificate management may not work properly.`
            );
          } else {
            // Create CertRequirement for automatic certificate management
            const now = Date.now();
            await this.database.createCertRequirement({
              serviceId: service.id!,
              domainId: domainRecord.id!,
              subdomain: subdomain,
              status: 'pending',
              createdAt: now,
              updatedAt: now,
            });
            logger.info(
              `Created certificate requirement for ${domain} ` +
              `(domain: ${baseDomain}, subdomain: ${subdomain || 'none'})`
            );
          }
        } catch (error) {
          logger.warn(`Failed to create certificate requirement: ${getErrorMessage(error)}`);
        }

        // Configure DNS (if autoDNS is enabled)
        if (options.autoDNS !== false) {
          try {
            const existingDnsRecord = await this.database.getDnsRecordByDomain(domain);
            await this.oneboxRef.dns.addDNSRecord(domain);
            if (!existingDnsRecord) deploymentSideEffects.dnsRecordDomains.add(domain);
          } catch (error) {
            logger.warn(`Failed to configure DNS for ${domain}: ${getErrorMessage(error)}`);
          }
        }

        // Configure reverse proxy
        try {
          await this.oneboxRef.reverseProxy.addRoute(
            service.id!,
            domain,
            this.resolveServiceRoutePort(serviceForRoutes, domain),
          );
          deploymentSideEffects.localRouteDomains.add(domain);
        } catch (error) {
          logger.warn(`Failed to configure reverse proxy for ${domain}: ${getErrorMessage(error)}`);
        }

        // Note: SSL certificates are now handled automatically by CertRequirementManager
        // which processes pending requirements created above. No direct obtainCertificate call needed.

      }

      const serviceForGatewaySync = (await this.database.getServiceByName(options.name))!;
      await this.oneboxRef.reverseProxy.reloadPublicPortRoutes?.();
      if (this.hasExternalGatewayRoutes(serviceForGatewaySync)) {
        deploymentSideEffects.externalGatewaySyncAttempted = true;
        await this.syncExternalGatewayRoute(serviceForGatewaySync);
      }

      if (imageCleanup) {
        await this.database.clearServiceImageCleanup(service.id!);
      }

      logger.success(`Service deployed successfully: ${options.name}`);

      const deployedService = (await this.database.getServiceByName(options.name))!;
      deploymentCommitted = true;
      await this.broadcastServiceUpdate(options.name, 'created');
      return deployedService;
    } catch (error) {
      if (createdService && !deploymentCommitted) {
        await this.rollbackFailedDeployment(
          createdService,
          createdRuntimeID,
          deploymentSideEffects,
          imageCleanup,
        ).catch((rollbackError) => {
          logger.error(
            `Failed to fully roll back service ${options.name}: ${getErrorMessage(rollbackError)}`,
          );
        });
      }
      logger.error(`Failed to deploy service ${options.name}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Start a service
   */
  async startService(name: string): Promise<void> {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service) {
        throw new Error(`Service not found: ${name}`);
      }

      if (!service.containerID) {
        throw new Error(`Service ${name} has no container ID`);
      }

      logger.info(`Starting service: ${name}`);

      await this.preflightPublicPortRoutes(service);

      await this.database.updateService(service.id!, { status: 'starting' });

      await this.docker.startContainer(service.containerID);

      await this.database.updateService(service.id!, { status: 'running' });

      // Add reverse proxy routes for canonical service domains.
      for (const domain of this.getServiceDomainNames(service)) {
        try {
          await this.oneboxRef.reverseProxy.addRoute(
            service.id!,
            domain,
            this.resolveServiceRoutePort(service, domain),
          );
        } catch (routeError) {
          logger.warn(`Failed to add proxy route for ${domain}: ${getErrorMessage(routeError)}`);
        }
      }

      const serviceForGatewaySync = (await this.database.getServiceByName(name))!;
      await this.oneboxRef.reverseProxy.reloadPublicPortRoutes?.();
      if (this.hasExternalGatewayRoutes(serviceForGatewaySync)) {
        await this.syncExternalGatewayRoute(serviceForGatewaySync);
      }

      await this.oneboxRef.opsServer?.telemetryHandler?.refreshServiceTelemetry?.(name);
      logger.success(`Service started: ${name}`);
      await this.broadcastServiceUpdate(name, 'started');
    } catch (error) {
      logger.error(`Failed to start service ${name}: ${getErrorMessage(error)}`);
      const failedService = await this.database.getServiceByName(name);
      if (failedService?.id) {
        await this.database.updateService(failedService.id, { status: 'failed' });
      }
      await this.broadcastServiceUpdate(name, 'updated');
      throw error;
    }
  }

  /**
   * Stop a service
   */
  async stopService(name: string): Promise<void> {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service) {
        throw new Error(`Service not found: ${name}`);
      }

      if (!service.containerID) {
        throw new Error(`Service ${name} has no container ID`);
      }

      logger.info(`Stopping service: ${name}`);

      await this.deleteExternalGatewayRoutesStrict(service);
      await this.deleteExternalGatewayPublicRoutesStrict(service);

      await this.database.updateService(service.id!, { status: 'stopping' });

      await this.docker.stopContainer(service.containerID);

      await this.database.updateService(service.id!, { status: 'stopped' });
      this.oneboxRef.opsServer?.telemetryHandler?.stopServiceTelemetry?.(name);

      // Remove reverse proxy routes for canonical service domains.
      for (const domain of this.getServiceDomainNames(service)) {
        await this.oneboxRef.reverseProxy.removeRoute(domain);
      }
      await this.oneboxRef.reverseProxy.reloadPublicPortRoutes?.();

      logger.success(`Service stopped: ${name}`);
      await this.broadcastServiceUpdate(name, 'stopped');
    } catch (error) {
      logger.error(`Failed to stop service ${name}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Restart a service
   */
  async restartService(name: string): Promise<void> {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service) {
        throw new Error(`Service not found: ${name}`);
      }

      if (!service.containerID) {
        throw new Error(`Service ${name} has no container ID`);
      }

      logger.info(`Restarting service: ${name}`);

      await this.docker.restartContainer(service.containerID);

      await this.database.updateService(service.id!, { status: 'running' });

      await this.oneboxRef.opsServer?.telemetryHandler?.refreshServiceTelemetry?.(name);
      logger.success(`Service restarted: ${name}`);
      await this.broadcastServiceUpdate(name, 'updated');
    } catch (error) {
      logger.error(`Failed to restart service ${name}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Remove a service (full cleanup)
   */
  async removeService(name: string): Promise<void> {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service) {
        throw new Error(`Service not found: ${name}`);
      }

      logger.info(`Removing service: ${name}`);

      const serviceId = service.id!;
      const serviceCertRequirements = await this.database.getCertRequirementsByService(serviceId);

      this.oneboxRef.opsServer?.telemetryHandler?.removeServiceTelemetry?.(name);
      this.oneboxRef.opsServer?.platformHandler?.stopLogStreamForService?.(name);
      await this.oneboxRef.opsServer?.workspaceHandler?.closeProcessesForService?.(name);
      this.oneboxRef.opsServer?.appStoreHandler?.clearOperationsForService?.(name);

      await this.deleteExternalGatewayRoutesStrict(service);
      await this.deleteExternalGatewayPublicRoutesStrict(service);
      await this.oneboxRef.reverseProxy.reloadPublicPortRoutes?.({ excludeServiceId: serviceId });

      const runtimeReferences = await this.docker.removeServiceRuntime(service);

      // Remove reverse proxy routes and domain-owned records.
      for (const domain of this.getServiceDomainNames(service)) {
        await this.oneboxRef.reverseProxy.removeRoute(domain);
        await this.removeDnsRecordIfServiceOwned(service, domain);
        if (!(await this.isDomainUsedByOtherService(service, domain))) {
          await this.oneboxRef.reverseProxy.removeCertificate(domain);
        }
      }

      const platformResources = await this.database.getPlatformResourcesByService(serviceId);
      if (platformResources.length > 0) {
        logger.info(`Cleaning up platform resources for service '${name}'...`);
        const platformServices = this.oneboxRef.platformServices as PlatformServicesManager;
        await platformServices.cleanupForService(serviceId);
        logger.success(`Platform resources cleaned up for service '${name}'`);
      }

      await this.oneboxRef.backupScheduler?.deleteSchedulesForService?.(serviceId);
      await this.oneboxRef.backupManager?.deleteBackupsForService?.(serviceId);
      await this.database.cleanupRegistryTokensForService(name);

      await this.docker.removeServiceOwnedVolumes(service, runtimeReferences.volumeNames);

      await this.cleanupOneboxRegistryImage(service);
      if (!(await this.isImageReferencedByOtherService(service, runtimeReferences.imageName))) {
        await this.docker.removeImageIfUnused(runtimeReferences.imageName);
      }
      if (service.imageCleanup) {
        for (const imageReference of new Set([
          service.imageCleanup.candidateReference,
          service.imageCleanup.priorReference,
        ].filter((referenceArg): referenceArg is string => Boolean(referenceArg)))) {
          await this.docker.removeImageIfUnused(imageReference);
        }
      }

      // Remove from database
      await this.database.deleteService(serviceId);
      await this.oneboxRef.reverseProxy.reloadPublicPortRoutes?.();
      await this.cleanupUnreferencedCertificates(serviceCertRequirements);

      logger.success(`Service removed: ${name}`);
      await this.oneboxRef.opsServer.broadcastServiceUpdate(name, 'deleted');
    } catch (error) {
      logger.error(`Failed to remove service ${name}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * List all services
   */
  async listServices(): Promise<IService[]> {
    return await this.database.getAllServices();
  }

  /**
   * Get service by name
   */
  async getService(name: string): Promise<IService | null> {
    return await this.database.getServiceByName(name);
  }

  /**
   * Get service logs
   */
  async getServiceLogs(name: string, tail = 100): Promise<string> {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service) {
        throw new Error(`Service not found: ${name}`);
      }

      if (!service.containerID) {
        throw new Error(`Service ${name} has no container ID`);
      }

      const logEntries = await this.docker.getContainerLogEntries(service.containerID, tail);
      return logEntries.map((entry) => entry.content).join('');
    } catch (error) {
      logger.error(`Failed to get logs for service ${name}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Stream service logs (real-time)
   */
  async streamServiceLogs(
    name: string,
    callback: (line: string, isError: boolean) => void
  ): Promise<void> {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service) {
        throw new Error(`Service not found: ${name}`);
      }

      if (!service.containerID) {
        throw new Error(`Service ${name} has no container ID`);
      }

      await this.docker.streamContainerLogs(service.containerID, callback);
    } catch (error) {
      logger.error(`Failed to stream logs for service ${name}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Get service metrics
   */
  async getServiceMetrics(name: string) {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service) {
        throw new Error(`Service not found: ${name}`);
      }

      if (!service.containerID) {
        throw new Error(`Service ${name} has no container ID`);
      }

      const stats = await this.docker.getContainerStats(service.containerID);
      return stats;
    } catch (error) {
      logger.error(`Failed to get metrics for service ${name}: ${getErrorMessage(error)}`);
      return null;
    }
  }

  /**
   * Get platform resources for a service
   */
  async getServicePlatformResources(name: string) {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service) {
        throw new Error(`Service not found: ${name}`);
      }

      if (!service.platformRequirements) {
        return [];
      }

      const platformServices = this.oneboxRef.platformServices as PlatformServicesManager;
      return await platformServices.getResourcesForService(service.id!);
    } catch (error) {
      logger.error(`Failed to get platform resources for service ${name}: ${getErrorMessage(error)}`);
      return [];
    }
  }

  /**
   * Get service status
   */
  async getServiceStatus(name: string): Promise<string> {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service) {
        return 'not-found';
      }

      if (!service.containerID) {
        return service.status;
      }

      const status = await this.docker.getContainerStatus(service.containerID);
      return status;
    } catch (error) {
      logger.error(`Failed to get status for service ${name}: ${getErrorMessage(error)}`);
      return 'unknown';
    }
  }

  /**
   * Update service environment variables
   */
  async updateServiceEnv(name: string, envVars: Record<string, string>): Promise<void> {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service) {
        throw new Error(`Service not found: ${name}`);
      }

      // Update database
      await this.database.updateService(service.id!, { envVars });

      // Note: Requires container restart to take effect
      logger.info(`Environment variables updated for ${name}. Restart service to apply changes.`);
    } catch (error) {
      logger.error(`Failed to update env vars for service ${name}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  private async rollbackServiceUpdate(argsArg: {
    service: IService;
    serviceSecrets: Record<string, string>;
    oldRuntimeRemoved: boolean;
    replacementContainerID?: string;
    candidateService?: IService;
    priorRuntimeReferences?: { volumeNames: string[]; imageReference?: string };
    replacementCreationAttempted?: boolean;
    imageCleanup?: IService['imageCleanup'];
  }): Promise<void> {
    const { service, serviceSecrets, oldRuntimeRemoved, replacementContainerID } = argsArg;
    const candidateService = argsArg.candidateService || {
      ...service,
      containerID: replacementContainerID,
    };
    if (argsArg.replacementCreationAttempted || replacementContainerID || oldRuntimeRemoved) {
      try {
        const candidateRuntimeReferences = await this.docker.collectServiceRuntimeReferences?.({
          ...candidateService,
          containerID: replacementContainerID || candidateService.containerID,
        });
        await this.docker.removeServiceRuntimeForReplacement({
          ...candidateService,
          containerID: replacementContainerID || candidateService.containerID,
        });
        if (candidateRuntimeReferences && this.docker.removeCandidateOnlyServiceVolumes) {
          await this.docker.removeCandidateOnlyServiceVolumes(
            candidateService,
            argsArg.priorRuntimeReferences?.volumeNames || [],
            candidateRuntimeReferences.volumeNames,
          );
        }
      } catch (error) {
        const failures = [`replacement runtime: ${getErrorMessage(error)}`];
        try {
          await this.database.markServiceRuntimeCleanupRequired(service.id!, replacementContainerID);
        } catch (markError) {
          failures.push(`failed runtime ownership marker: ${getErrorMessage(markError)}`);
        }
        throw new AggregateError(failures, `Replacement runtime cleanup failed for ${service.name}`);
      }
    }

    const failures: string[] = [];
    const attempt = async (labelArg: string, actionArg: () => Promise<unknown>) => {
      try {
        await actionArg();
      } catch (error) {
        failures.push(`${labelArg}: ${getErrorMessage(error)}`);
      }
    };
    const oldDomains = new Set(this.getServiceDomainNames(service));
    for (const candidateDomain of this.getServiceDomainNames(candidateService)) {
      if (oldDomains.has(candidateDomain)) continue;
      await attempt(`candidate local route ${candidateDomain}`, async () => {
        await this.oneboxRef.reverseProxy.removeRoute?.(candidateDomain);
      });
      await attempt(`candidate external route ${candidateDomain}`, () => (
        this.deleteExternalGatewayRouteStrict({ ...candidateService, domain: candidateDomain })
      ));
    }
    const candidateOnlyPublicMappings = this.getStalePublicPortMappings(candidateService, service);
    if (candidateOnlyPublicMappings.length > 0) {
      await attempt('candidate public routes', () => this.deleteExternalGatewayPublicRoutesStrict({
        ...candidateService,
        publicPortMappings: candidateOnlyPublicMappings,
      }));
    }

    if (failures.length > 0) {
      await attempt('failed candidate ownership metadata', () => this.database.replaceService(service.id!, {
        ...candidateService,
        containerID: undefined,
        status: 'failed',
        updatedAt: Date.now(),
      }));
      throw new AggregateError(
        failures,
        `Candidate route cleanup failed for ${service.name}; candidate ownership was retained`,
      );
    }

    await attempt('prior service metadata', () => this.database.replaceService(service.id!, service));
    await attempt('prior service secrets', () => this.database.serviceSecrets.set(service.id!, serviceSecrets));

    let restoredContainerID = service.containerID;
    if (service.containerID && oldRuntimeRemoved) {
      try {
        const restorationService = argsArg.priorRuntimeReferences?.imageReference
          ? {
              ...service,
              image: argsArg.priorRuntimeReferences.imageReference,
              registry: undefined,
            }
          : service;
        restoredContainerID = await this.docker.createContainer(restorationService, serviceSecrets);
      } catch (error) {
        restoredContainerID = undefined;
        failures.push(`prior runtime recreation: ${getErrorMessage(error)}`);
      }
    }
    if (restoredContainerID) {
      if (service.status === 'running' || service.status === 'starting') {
        await attempt('prior runtime start', () => this.docker.startContainer(restoredContainerID!));
      } else if (oldRuntimeRemoved) {
        await attempt('prior runtime stop', () => this.docker.stopContainer(restoredContainerID!));
      }
    }
    await attempt('restored service metadata', () => this.database.replaceService(service.id!, {
      ...service,
      containerID: restoredContainerID,
      status: restoredContainerID ? service.status : 'failed',
    }));

    if (service.status === 'running' && restoredContainerID) {
      for (const domain of this.getServiceDomainNames(service)) {
        await attempt(`prior local route ${domain}`, () => this.oneboxRef.reverseProxy.addRoute(
          service.id!,
          domain,
          this.resolveServiceRoutePort(service, domain),
        ));
      }
      await attempt('prior public routes', async () => {
        await this.oneboxRef.reverseProxy.reloadPublicPortRoutes?.();
      });
      if (this.hasExternalGatewayRoutes(service)) {
        await attempt('prior external gateway routes', () => this.syncExternalGatewayRoute(service, { strict: true }));
      }
    }
    if (argsArg.imageCleanup) {
      try {
        await this.rollbackPulledImage(argsArg.imageCleanup);
      } catch (error) {
        failures.push(`unused pulled image: ${getErrorMessage(error)}`);
        await attempt('image cleanup ownership', () => this.database.updateService(service.id!, {
          imageCleanup: argsArg.imageCleanup,
          status: restoredContainerID ? service.status : 'failed',
          containerID: restoredContainerID,
        }));
      }
    }
    if (failures.length > 0) {
      if (!restoredContainerID) {
        await attempt('failed runtime ownership marker', () => (
          this.database.markServiceRuntimeCleanupRequired(service.id!)
        ));
      }
      throw new AggregateError(failures, `Failed to fully roll back service update ${service.name}`);
    }
  }

  /**
   * Update service configuration (image, port, domain, env vars)
   * Recreates the container with new configuration and auto-restarts
   */
  async updateService(
    name: string,
    updates: {
      image?: string;
      registry?: string;
      port?: number;
      domain?: string;
      envVars?: Record<string, string>;
      secretEnvVars?: Record<string, string>;
      containerArgs?: string[] | null;
      secretFiles?: IService['secretFiles'] | null;
      volumes?: IService['volumes'];
      publishedPorts?: IService['publishedPorts'];
      targetPorts?: IService['targetPorts'];
      domains?: IService['domains'];
      publicPortMappings?: IService['publicPortMappings'];
      imageDigest?: string;
      appTemplateVersion?: string;
      forcePull?: boolean;
    },
    optionsArg: IServiceUpdateOptions = {},
  ): Promise<IService> {
    let rollbackService: IService | null = null;
    let rollbackSecrets: Record<string, string> = {};
    let updateMutationStarted = false;
    let updateCommitted = false;
    let oldRuntimeRemoved = false;
    let replacementContainerID: string | undefined;
    let rollbackCandidateService: IService | undefined;
    let rollbackRuntimeReferences: { volumeNames: string[]; imageReference?: string } | undefined;
    let replacementCreationAttempted = false;
    let imageCleanup: IService['imageCleanup'];
    try {
      const emitProgress = async (step: TServiceUpdateProgressStep, message: string) => {
        await optionsArg.onProgress?.({ step, message });
      };
      let service = await this.database.getServiceByName(name);
      if (!service) {
        throw new Error(`Service not found: ${name}`);
      }
      if (service.imageCleanup) {
        await this.recoverPendingImageCleanup(service);
        service = (await this.database.getServiceByName(name))!;
      }
      rollbackService = service.toData();
      rollbackSecrets = await this.database.serviceSecrets.get(service.id!);
      const nextServiceSecrets = updates.secretEnvVars === undefined
        ? rollbackSecrets
        : this.database.serviceSecrets.validate(updates.secretEnvVars);

      logger.info(`Updating service: ${name}`);
      const wasRunning = service.status === 'running';
      const oldContainerID = service.containerID;
      const oldDomainNames = this.getServiceDomainNames(service);
      const imageToPull = updates.image || service.image;
      const registryToPull = updates.registry !== undefined ? updates.registry : service.registry;
      const shouldPullImage = updates.forcePull || (updates.image !== undefined && updates.image !== service.image);
      const updatesDomainRoutes = updates.domain !== undefined || updates.domains !== undefined;
      const updatesPublicPortMappings = updates.publicPortMappings !== undefined;

      const updateData: any = {
        updatedAt: Date.now(),
      };
      if (updates.image !== undefined) updateData.image = updates.image;
      if (updates.registry !== undefined) updateData.registry = updates.registry;
      if (updates.port !== undefined) updateData.port = updates.port;
      if (updates.domain !== undefined) updateData.domain = updates.domain;
      if (updates.envVars !== undefined) updateData.envVars = updates.envVars;
      if (updates.containerArgs !== undefined) updateData.containerArgs = updates.containerArgs;
      if (updates.secretFiles !== undefined) updateData.secretFiles = updates.secretFiles;
      if (updates.volumes !== undefined) updateData.volumes = await this.resolveStorageVolumes(updates.volumes, 'runtime');
      if (updates.publishedPorts !== undefined) updateData.publishedPorts = updates.publishedPorts;
      if (updates.targetPorts !== undefined) updateData.targetPorts = updates.targetPorts;
      if (updates.domains !== undefined) updateData.domains = updates.domains;
      if (updates.publicPortMappings !== undefined) updateData.publicPortMappings = updates.publicPortMappings;
      if (updates.imageDigest !== undefined) updateData.imageDigest = updates.imageDigest;
      if (updates.appTemplateVersion !== undefined) updateData.appTemplateVersion = updates.appTemplateVersion;

      const preflightService: IService = {
        ...service,
        ...updateData,
        containerArgs: updates.containerArgs === null ? undefined : (updates.containerArgs ?? service.containerArgs),
        secretFiles: updates.secretFiles === null ? undefined : (updates.secretFiles ?? service.secretFiles),
        status: wasRunning ? 'running' : service.status,
        containerID: service.containerID || '__pending__',
      };
      this.docker.validateServiceSpec(preflightService);
      await this.docker.assertSecretFilesSupported(preflightService.secretFiles);
      this.docker.validateServiceSecrets(preflightService, nextServiceSecrets);
      await this.preflightPublicPortRoutes(preflightService);

      if (oldContainerID && this.docker.collectServiceRuntimeReferences) {
        rollbackRuntimeReferences = await this.docker.collectServiceRuntimeReferences(service);
      }

      if (shouldPullImage) {
        logger.info(`Pulling image: ${imageToPull}`);
        await emitProgress('pulling-image', `Pulling image ${imageToPull}`);
        const priorImageReference = await this.docker.getImageReference?.(imageToPull, registryToPull);
        await this.docker.pullImage(imageToPull, registryToPull);
        const candidateImageReference = await this.docker.getImageReference?.(imageToPull, registryToPull);
        if (candidateImageReference && candidateImageReference !== priorImageReference) {
          imageCleanup = {
            imageName: registryToPull ? `${registryToPull}/${imageToPull}` : imageToPull,
            candidateReference: candidateImageReference,
            priorReference: priorImageReference,
            phase: 'rollback-pending',
          };
          updateMutationStarted = true;
          await this.database.updateService(service.id!, { imageCleanup });
        }
      }

      if (oldContainerID) {
        await this.oneboxRef.opsServer?.telemetryHandler?.prepareServiceRuntimeReplacement?.(name);
      }
      updateMutationStarted = true;

      // Stop the container if running
      if (wasRunning && oldContainerID) {
        logger.info(`Stopping service ${name} for updates...`);
        await emitProgress('stopping', `Stopping ${name} before updating its container`);
        await this.docker.stopContainer(oldContainerID);
      }

      // Update service in database
      await emitProgress('updating-record', `Updating service record for ${name}`);
      await this.database.updateService(service.id!, updateData);
      if (updates.secretEnvVars !== undefined) {
        await this.database.serviceSecrets.set(service.id!, nextServiceSecrets);
      }

      // Get updated service
      const updatedService = (await this.database.getServiceByName(name))!;
      rollbackCandidateService = updatedService.toData();

      // Remove old container
      if (oldContainerID) {
        await emitProgress('removing-container', `Removing old container for ${name}`);
        await this.docker.removeServiceRuntimeForReplacement(service, () => {
          oldRuntimeRemoved = true;
        });
        oldRuntimeRemoved = true;
        logger.info(`Removed old container for ${name}`);
      }

      // Create new container with updated config
      logger.info(`Creating new container for ${name}...`);
      await emitProgress('creating-container', `Creating replacement container for ${name}`);
      replacementCreationAttempted = true;
      const containerID = await this.docker.createContainer(updatedService, nextServiceSecrets);
      replacementContainerID = containerID;
      rollbackCandidateService = { ...updatedService.toData(), containerID };
      await this.database.updateService(service.id!, {
        containerID,
        ...(imageCleanup
          ? { imageCleanup: { ...imageCleanup, phase: 'candidate-active' as const } }
          : {}),
      });

      // Remove old local reverse proxy routes before restoring routes from refreshed service data.
      if (updatesDomainRoutes) {
        for (const oldDomain of oldDomainNames) {
          try {
            await this.oneboxRef.reverseProxy.removeRoute(oldDomain);
          } catch (error) {
            logger.warn(`Failed to remove old reverse proxy route: ${getErrorMessage(error)}`);
          }
        }
      }

      // Restart the container if it was running
      if (wasRunning) {
        logger.info(`Starting updated service ${name}...`);
        await emitProgress('starting', `Starting updated service ${name}`);
        await this.database.updateService(service.id!, { status: 'starting' });
        await this.docker.startContainer(containerID);
        await this.database.updateService(service.id!, { status: 'running' });
        await this.oneboxRef.opsServer?.telemetryHandler?.refreshServiceTelemetry?.(name);
        logger.success(`Service ${name} updated and restarted`);
      } else {
        await this.docker.stopContainer(containerID);
        await this.database.updateService(service.id!, { status: 'stopped' });
        this.oneboxRef.opsServer?.telemetryHandler?.stopServiceTelemetry?.(name);
        logger.success(`Service ${name} updated (not started)`);
      }

      const refreshedService = (await this.database.getServiceByName(name))!;
      rollbackCandidateService = refreshedService.toData();
      if (refreshedService.status === 'running') {
        for (const domain of this.getServiceDomainNames(refreshedService)) {
          const refreshedRoutePort = this.resolveServiceRoutePort(refreshedService, domain);
          await emitProgress('restoring-route', `Restoring route ${domain} -> ${refreshedRoutePort}`);
          try {
            await this.oneboxRef.reverseProxy.addRoute(
              refreshedService.id!,
              domain,
              refreshedRoutePort,
            );
          } catch (error) {
            logger.warn(`Failed to restore reverse proxy route for ${domain}: ${getErrorMessage(error)}`);
            throw error;
          }
        }
      }
      if (refreshedService.status === 'running' && this.hasExternalGatewayRoutes(refreshedService)) {
        await this.oneboxRef.reverseProxy.reloadPublicPortRoutes?.();
        await emitProgress('syncing-gateway', `Syncing external gateway routes for ${refreshedService.name}`);
        await this.syncExternalGatewayRoute(refreshedService, { strict: true });
      } else {
        await this.oneboxRef.reverseProxy.reloadPublicPortRoutes?.();
      }

      if (updatesDomainRoutes) {
        const nextDomainNames = new Set(this.getServiceDomainNames(refreshedService));
        for (const oldDomain of oldDomainNames) {
          if (nextDomainNames.has(oldDomain)) continue;
          await this.deleteExternalGatewayRouteStrict({ ...service, domain: oldDomain });
        }
      }
      if (updatesPublicPortMappings) {
        await this.deleteExternalGatewayPublicRoutesStrict({
          ...service,
          publicPortMappings: this.getStalePublicPortMappings(service, refreshedService),
        });
      }

      if (imageCleanup) {
        await this.database.clearServiceImageCleanup(service.id!);
      }

      await emitProgress('complete', `Service ${name} update completed`);
      updateCommitted = true;
      await this.broadcastServiceUpdate(name, 'updated');
      if (shouldPullImage) {
        // Image updates leave the previous image dangling; reclaim it so
        // repeated updates do not leak disk on the host.
        void this.docker.pruneImages().catch((error) => {
          logger.warn(`Image prune after update failed: ${getErrorMessage(error)}`);
        });
      }
      return refreshedService;
    } catch (error) {
      if (rollbackService && updateMutationStarted && !updateCommitted) {
        await this.rollbackServiceUpdate({
          service: rollbackService,
          serviceSecrets: rollbackSecrets,
          oldRuntimeRemoved,
          replacementContainerID,
          candidateService: rollbackCandidateService,
          priorRuntimeReferences: rollbackRuntimeReferences,
          replacementCreationAttempted,
          imageCleanup,
        }).catch((rollbackError) => {
          logger.error(`Failed to roll back service ${name}: ${getErrorMessage(rollbackError)}`);
        });
      }
      logger.error(`Failed to update service ${name}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Sync service status from Docker
   */
  async syncServiceStatus(name: string): Promise<void> {
    try {
      const service = await this.database.getServiceByName(name);
      if (!service || !service.containerID) {
        return;
      }

      const status = await this.docker.getContainerStatus(service.containerID);

      // Map Docker status to our status
      let ourStatus: IService['status'] = 'stopped';
      if (status === 'running') {
        ourStatus = 'running';
      } else if (status === 'exited' || status === 'dead') {
        ourStatus = 'stopped';
      } else if (status === 'created') {
        ourStatus = 'stopped';
      } else if (status === 'restarting') {
        ourStatus = 'starting';
      }

      // Only update and broadcast if status changed
      if (service.status !== ourStatus) {
        await this.database.updateService(service.id!, { status: ourStatus });
        await this.broadcastServiceUpdate(name, 'updated');
      }
    } catch (error) {
      logger.debug(`Failed to sync status for service ${name}: ${getErrorMessage(error)}`);
    }
  }

  /**
   * Sync all service statuses from Docker
   */
  async syncAllServiceStatuses(): Promise<void> {
    const services = await this.listServices();

    for (const service of services) {
      await this.syncServiceStatus(service.name);
    }
  }

  /**
   * Start auto-update monitoring for registry services
   * Polls every 30 seconds for digest changes and restarts services if needed
   */
  startAutoUpdateMonitoring(): void {
    // Check every 30 seconds
    this.autoUpdateIntervalId = setInterval(async () => {
      try {
        await this.checkForRegistryUpdates();
      } catch (error) {
        logger.error(`Auto-update check failed: ${getErrorMessage(error)}`);
      }
    }, 30000);

    logger.info('Auto-update monitoring started (30s interval)');
  }

  /**
   * Stop auto-update monitoring
   */
  stopAutoUpdateMonitoring(): void {
    if (this.autoUpdateIntervalId !== null) {
      clearInterval(this.autoUpdateIntervalId);
      this.autoUpdateIntervalId = null;
      logger.debug('Auto-update monitoring stopped');
    }
  }

  /**
   * Check all services using onebox registry for updates
   */
  private async checkForRegistryUpdates(): Promise<void> {
    const services = await this.listServices();

    for (const service of services) {
      // Skip if not using onebox registry or auto-update is disabled
      if (!service.useOneboxRegistry || !service.autoUpdateOnPush) {
        continue;
      }

      try {
        // Get current digest from registry
        const currentDigest = await this.oneboxRef.registry.getImageDigest(
          service.registryRepository!,
          service.registryImageTag || 'latest'
        );

        // Skip if no digest found (image might not exist yet)
        if (!currentDigest) {
          continue;
        }

        // Check if digest has changed
        if (service.imageDigest && service.imageDigest !== currentDigest) {
          logger.info(
            `Digest changed for ${service.name}: ${service.imageDigest} -> ${currentDigest}`
          );

          // Update digest in database
          await this.database.updateService(service.id!, {
            imageDigest: currentDigest,
          });

          // Pull new image
          const imageName = this.oneboxRef.registry.getImageName(
            service.registryRepository!,
            service.registryImageTag || 'latest'
          );

          logger.info(`Pulling updated image: ${imageName}`);
          await this.docker.pullImage(imageName);

          // Restart service
          logger.info(`Auto-restarting service: ${service.name}`);
          await this.restartService(service.name);
        } else if (!service.imageDigest) {
          // First time - just store the digest
          await this.database.updateService(service.id!, {
            imageDigest: currentDigest,
          });
        }
      } catch (error) {
        logger.error(`Failed to check updates for ${service.name}: ${getErrorMessage(error)}`);
      }
    }
  }
}
