import * as plugins from '../plugins.js';
import { logger } from '../logging.js';
import { getErrorMessage } from '../utils/error.js';
import { normalizeHostname } from '../utils/domain.js';
import { OneboxDatabase } from '../database/index.js';
import type { IDomain, IService, IServiceDomainRoute } from '../types.js';
import type { TDcRouterMode } from './managed-dcrouter.js';

const adminUiRouteName = 'onebox-admin-ui';

type TExternalGatewayRoute = Pick<IService, 'id' | 'name' | 'domain' | 'status'> & {
  domain: string;
  sourceProfileRef?: string;
};

type TExternalGatewayDomainRoute = Pick<IServiceDomainRoute, 'name' | 'sourceProfileRef'>;

interface IExternalGatewayConfig {
  url: string;
  apiToken: string;
  gatewayClientType?: 'onebox';
  gatewayClientId?: string;
  targetHost?: string;
  targetPort?: number;
}

// Canonical gateway contracts come from @serve.zone/interfaces. Onebox adds a
// derived manageUrl (gateway URL + managePath) to domain and DNS record views.
type TGatewayDomainView = plugins.servezoneInterfaces.data.IGatewayDomain & {
  manageUrl?: string;
};

type TGatewayDnsRecordView = plugins.servezoneInterfaces.data.IGatewayDnsRecord & {
  manageUrl?: string;
};

type TGatewayClientRouteView = plugins.servezoneInterfaces.data.IGatewayClientRoute & {
  manageUrl?: string;
};


export class ExternalGatewayManager {
  private database: OneboxDatabase;

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

  public async init(): Promise<void> {
    if (!(await this.isConfigured())) {
      logger.info('External dcrouter gateway not configured');
      return;
    }

    await this.syncDomains();
    await this.syncServiceRoutes();
  }

  public async syncServiceRoutes(): Promise<void> {
    const adminUiRoute = await this.getAdminUiRoute();
    const adminUiDomain = adminUiRoute?.domain;
    const services = (await this.database.getAllServices()).filter((service) => {
      if (service.status !== 'running') return false;
      const domainRoutes = this.getServiceDomainRoutes(service).filter((domainArg) => domainArg.name !== adminUiDomain);
      return domainRoutes.length > 0 || Boolean(service.publicPortMappings?.length);
    });
    const activeHostnames = new Set<string>();
    const activePublicRouteOwnershipKeys = new Set<string>();

    if (adminUiRoute) {
      activeHostnames.add(adminUiRoute.domain);
      try {
        await this.syncGatewayRoute(adminUiRoute);
      } catch (error) {
        logger.warn(
          `Failed to sync external gateway route for ${adminUiRoute.domain}: ${
            getErrorMessage(error)
          }`,
        );
      }
    }

    for (const service of services) {
      try {
        const domainRoutes = this.getServiceDomainRoutes(service).filter((domainArg) => domainArg.name !== adminUiDomain);
        for (const domainRoute of domainRoutes) {
          activeHostnames.add(domainRoute.name);
        }
        for (const routeRef of this.getServicePublicRouteRefs(service)) {
          activePublicRouteOwnershipKeys.add(this.publicRouteOwnershipKey(service.name, routeRef));
        }
        await this.syncServiceRoute(service, domainRoutes);
      } catch (error) {
        logger.warn(
          `Failed to sync external gateway route for ${service.name}: ${getErrorMessage(error)}`,
        );
      }
    }

    await this.deleteStaleServiceRoutes(activeHostnames, activePublicRouteOwnershipKeys);
  }

  private async deleteStaleServiceRoutes(
    activeHostnamesArg: Set<string>,
    activePublicRouteOwnershipKeysArg: Set<string>,
  ): Promise<void> {
    const records = await this.getGatewayDnsRecords();
    const staleRecordsByHostname = new Map<string, TGatewayDnsRecordView>();

    for (const record of records) {
      if (!record.hostname || activeHostnamesArg.has(record.hostname)) continue;
      if (await this.shouldPreserveUnconfiguredAdminUiRecord(record)) continue;
      if (!record.routeId && !record.appId && !record.serviceName) continue;
      staleRecordsByHostname.set(record.hostname, record);
    }

    for (const record of staleRecordsByHostname.values()) {
      try {
        await this.deleteServiceRoute({
          name: record.serviceName || record.appId,
          domain: record.hostname,
        });
      } catch (error) {
        logger.warn(
          `Failed to delete stale external gateway route for ${record.hostname}: ${
            getErrorMessage(error)
          }`,
        );
      }
    }

    const routes = await this.getGatewayClientRoutes();
    const config = routes.some((routeArg) => this.isStalePublicRoute(routeArg, activePublicRouteOwnershipKeysArg))
      ? await this.getConfig({ requireTarget: false })
      : null;
    if (!config) return;
    for (const route of routes) {
      const routeRef = route.ownership.routeRef;
      if (!this.isStalePublicRoute(route, activePublicRouteOwnershipKeysArg)) continue;
      try {
        await this.deleteGatewayRouteRef(route, config);
      } catch (error) {
        logger.warn(
          `Failed to delete stale external gateway public route ${routeRef}: ${
            getErrorMessage(error)
          }`,
        );
      }
    }
  }

  public async isConfigured(): Promise<boolean> {
    if ((await this.getMode()) === 'disabled') {
      return false;
    }
    const mode = await this.getMode();
    const url = mode === 'managed'
      ? await this.oneboxRef.managedDcRouter.getGatewayUrl()
      : this.normalizeUrl((await this.database.getSetting('dcrouterGatewayUrl')) || '');
    const apiToken = mode === 'managed'
      ? await this.oneboxRef.managedDcRouter.getAdminToken()
      : await this.database.getSecretSetting('dcrouterGatewayApiToken');
    return Boolean(url && apiToken);
  }

  public async syncDomains(): Promise<IDomain[]> {
    if (!(await this.isConfigured())) {
      return await this.database.getDomainsByProvider('dcrouter');
    }
    const response = { domains: await this.getGatewayDomains() };

    const activeDomainNames = new Set<string>();
    const now = Date.now();

    for (const gatewayDomain of response.domains) {
      const domainName = gatewayDomain.name.trim().toLowerCase();
      if (!domainName) continue;

      activeDomainNames.add(domainName);
      const existingDomain = await this.database.getDomainByName(domainName);
      const defaultWildcard = gatewayDomain.capabilities?.canIssueCertificates !== false;

      if (existingDomain) {
        await this.database.updateDomain(existingDomain.id!, {
          dnsProvider: 'dcrouter',
          isObsolete: false,
          defaultWildcard,
          updatedAt: now,
        });
      } else {
        await this.database.createDomain({
          domain: domainName,
          dnsProvider: 'dcrouter',
          isObsolete: false,
          defaultWildcard,
          createdAt: now,
          updatedAt: now,
        });
      }
    }

    for (const domain of await this.database.getDomainsByProvider('dcrouter')) {
      if (!activeDomainNames.has(domain.domain)) {
        await this.database.updateDomain(domain.id!, {
          isObsolete: true,
          updatedAt: now,
        });
      }
    }

    logger.success(`Synced ${activeDomainNames.size} domain(s) from external dcrouter gateway`);
    return await this.database.getDomainsByProvider('dcrouter');
  }

  public async getGatewayDomains(): Promise<TGatewayDomainView[]> {
    const config = await this.getConfig({ requireTarget: false });
    if (!config) return [];

    const response = await this.fireDcRouterRequest<
      plugins.servezoneInterfaces.requests.gateway.IReq_GetGatewayClientDomains
    >(
      'getGatewayClientDomains',
      config.gatewayClientId ? { gatewayClientId: config.gatewayClientId } : {},
      config,
    );
    return response.domains.map((domain) => ({
      ...domain,
      manageUrl: this.buildManageUrl(config, domain.managePath),
    }));
  }

  public async getGatewayDnsRecords(): Promise<TGatewayDnsRecordView[]> {
    const config = await this.getConfig({ requireTarget: false });
    if (!config) return [];

    try {
      const response = await this.fireDcRouterRequest<
        plugins.servezoneInterfaces.requests.gateway.IReq_GetGatewayClientDnsRecords
      >(
        'getGatewayClientDnsRecords',
        config.gatewayClientId ? { gatewayClientId: config.gatewayClientId } : {},
        config,
      );
      return response.records.map((record) => ({
        ...record,
        serviceName: record.serviceName || record.appId,
        manageUrl: this.buildManageUrl(config, record.managePath),
      }));
    } catch (error) {
      logger.warn(`Failed to fetch gateway DNS records: ${getErrorMessage(error)}`);
      return [];
    }
  }

  public async getGatewayClientRoutes(): Promise<TGatewayClientRouteView[]> {
    const config = await this.getConfig({ requireTarget: false });
    if (!config) return [];

    try {
      const response = await this.fireDcRouterRequest<
        plugins.servezoneInterfaces.requests.gateway.IReq_GetGatewayClientRoutes
      >(
        'getGatewayClientRoutes',
        config.gatewayClientId ? { gatewayClientId: config.gatewayClientId } : {},
        config,
      );
      return response.routes.map((route) => ({
        ...route,
        manageUrl: this.buildManageUrl(config, route.managePath),
      }));
    } catch (error) {
      logger.warn(`Failed to fetch gateway client routes: ${getErrorMessage(error)}`);
      return [];
    }
  }

  public async syncServiceRoute(
    service: IService,
    domainRoutesArg = this.getServiceDomainRoutes(service),
  ): Promise<void> {
    for (const domainRoute of domainRoutesArg) {
      await this.syncGatewayRoute({
        id: service.id,
        name: service.name,
        domain: domainRoute.name,
        status: service.status,
        sourceProfileRef: domainRoute.sourceProfileRef,
      });
    }
    await this.syncServicePublicRoutes(service);
  }

  private async syncServicePublicRoutes(service: IService): Promise<void> {
    if (service.status !== 'running' || !service.publicPortMappings?.length) return;
    const config = await this.getConfig({ requireTarget: true });
    if (!config) return;
    for (const mapping of service.publicPortMappings) {
      const publicPortEnd = mapping.publicPortEnd || mapping.publicPort;
      for (let port = mapping.publicPort; port <= publicPortEnd; port++) {
        const routeRef = this.publicRouteRef(mapping.name, mapping.protocol || 'tcp', port);
        const result = await this.fireDcRouterRequest<
          plugins.servezoneInterfaces.requests.gateway.IReq_SyncGatewayClientRoute
        >(
          'syncGatewayClientRoute',
          {
            ownership: this.buildGatewayClientRouteRefOwnership(service, routeRef, config),
            route: {
              name: `onebox-${service.name}-${mapping.name}-${mapping.protocol}-${port}`
                .replace(/[^a-zA-Z0-9]+/g, '-')
                .replace(/^-|-$/g, '')
                .slice(0, 96),
              match: {
                ports: [port],
                transport: mapping.protocol,
              },
              action: {
                type: 'forward',
                targets: [{ host: config.targetHost!, port }],
              },
              remoteIngress: {
                enabled: true,
              },
            },
            enabled: true,
            ...(mapping.sourceProfileRef ? { sourceProfileRef: mapping.sourceProfileRef } : {}),
          },
          config,
        );
        if (!result.success) {
          throw new Error(result.message || `dcrouter public route sync failed for ${routeRef}`);
        }
      }
    }
  }

  public async deleteServicePublicRoutes(
    service: { id?: number; name?: string; publicPortMappings?: IService['publicPortMappings'] },
  ): Promise<void> {
    if (!service.publicPortMappings?.length) return;

    const config = await this.getConfig({ requireTarget: false });
    if (!config) return;

    for (const mapping of service.publicPortMappings) {
      const publicPortEnd = mapping.publicPortEnd || mapping.publicPort;
      for (let port = mapping.publicPort; port <= publicPortEnd; port++) {
        const routeRef = this.publicRouteRef(mapping.name, mapping.protocol || 'tcp', port);
        const result = await this.fireDcRouterRequest<
          plugins.servezoneInterfaces.requests.gateway.IReq_SyncGatewayClientRoute
        >(
          'syncGatewayClientRoute',
          {
            ownership: this.buildGatewayClientRouteRefOwnership(service, routeRef, config),
            delete: true,
          },
          config,
        );
        if (!result.success) {
          throw new Error(result.message || `dcrouter public route delete failed for ${routeRef}`);
        }
      }
    }
  }

  private async deleteGatewayRouteRef(
    routeArg: TGatewayClientRouteView,
    configArg: IExternalGatewayConfig,
  ): Promise<void> {
    const routeRef = routeArg.ownership.routeRef;
    if (!routeRef) return;
    const ownership: plugins.servezoneInterfaces.data.IGatewayClientOwnership = {
      gatewayClientType: routeArg.ownership.gatewayClientType || configArg.gatewayClientType || 'onebox',
      appId: routeArg.ownership.appId,
      routeRef,
    };
    if (routeArg.ownership.gatewayClientId || configArg.gatewayClientId) {
      ownership.gatewayClientId = routeArg.ownership.gatewayClientId || configArg.gatewayClientId;
    }
    const result = await this.fireDcRouterRequest<
      plugins.servezoneInterfaces.requests.gateway.IReq_SyncGatewayClientRoute
    >(
      'syncGatewayClientRoute',
      {
        ownership,
        delete: true,
      },
      configArg,
    );
    if (!result.success) {
      throw new Error(result.message || `dcrouter public route delete failed for ${routeRef}`);
    }
  }

  public async syncAdminUiRoute(): Promise<void> {
    const route = await this.getAdminUiRoute();
    if (!route) return;
    await this.syncGatewayRoute(route);
  }

  public async deleteAdminUiRoute(domain: string): Promise<void> {
    const normalizedDomain = normalizeHostname(domain);
    if (!normalizedDomain) return;
    await this.deleteServiceRoute({
      name: adminUiRouteName,
      domain: normalizedDomain,
    });
  }

  private async syncGatewayRoute(route: TExternalGatewayRoute): Promise<void> {
    if (!route.domain) return;

    const config = await this.getConfig({ requireTarget: true });
    if (!config) return;

    const result = await this.fireDcRouterRequest<
      plugins.servezoneInterfaces.requests.gateway.IReq_SyncGatewayClientRoute
    >(
      'syncGatewayClientRoute',
      {
        ownership: this.buildGatewayClientOwnership(route, route.domain, config),
        route: this.buildRoute(route, config),
        enabled: route.status === 'running',
        ...(route.sourceProfileRef ? { sourceProfileRef: route.sourceProfileRef } : {}),
      },
      config,
    );

    if (!result.success) {
      throw new Error(result.message || `dcrouter route sync failed for ${route.domain}`);
    }

    logger.success(`External gateway route ${result.action || 'synced'} for ${route.domain}`);
    await this.importCertificateForDomain(route.domain).catch((error) => {
      logger.debug(
        `External gateway certificate import skipped for ${route.domain}: ${
          getErrorMessage(error)
        }`,
      );
    });
  }

  public async deleteServiceRoute(
    service: { id?: number; name?: string; domain?: string },
  ): Promise<void> {
    if (!service.domain) return;

    const config = await this.getConfig({ requireTarget: false });
    if (!config) return;

    const result = await this.fireDcRouterRequest<
      plugins.servezoneInterfaces.requests.gateway.IReq_SyncGatewayClientRoute
    >(
      'syncGatewayClientRoute',
      {
        ownership: this.buildGatewayClientOwnership(service, service.domain, config),
        delete: true,
      },
      config,
    );

    if (!result.success) {
      throw new Error(result.message || `dcrouter route delete failed for ${service.domain}`);
    }

    logger.info(`External gateway route ${result.action || 'deleted'} for ${service.domain}`);
  }

  public async importCertificateForDomain(domain: string): Promise<boolean> {
    const config = await this.getConfig({ requireTarget: false });
    if (!config) return false;

    const result = await this.fireDcRouterRequest<
      plugins.servezoneInterfaces.requests.gateway.IReq_ExportCertificate
    >('exportCertificate', { domain }, config);

    if (!result.success || !result.cert) {
      return false;
    }

    const now = Date.now();
    const existingCertificate = await this.database.getSSLCertificate(domain);
    if (existingCertificate) {
      await this.database.updateSSLCertificate(domain, {
        certPem: result.cert.publicKey,
        keyPem: result.cert.privateKey,
        fullchainPem: result.cert.publicKey,
        expiryDate: result.cert.validUntil,
        updatedAt: now,
      });
    } else {
      await this.database.createSSLCertificate({
        domain,
        certPem: result.cert.publicKey,
        keyPem: result.cert.privateKey,
        fullchainPem: result.cert.publicKey,
        expiryDate: result.cert.validUntil,
        issuer: 'dcrouter',
        createdAt: now,
        updatedAt: now,
      });
    }

    await this.oneboxRef.reverseProxy.reloadCertificates();
    logger.success(`Imported external gateway certificate for ${domain}`);
    return true;
  }

  private async getConfig(options: { requireTarget?: boolean } = {}): Promise<IExternalGatewayConfig | null> {
    const mode = await this.getMode();
    if (mode === 'disabled') {
      return null;
    }

    const url = mode === 'managed'
      ? await this.oneboxRef.managedDcRouter.getGatewayUrl()
      : this.normalizeUrl((await this.database.getSetting('dcrouterGatewayUrl')) || '');
    const apiToken = mode === 'managed'
      ? await this.oneboxRef.managedDcRouter.getAdminToken()
      : await this.database.getSecretSetting('dcrouterGatewayApiToken');
    if (!url || !apiToken) {
      return null;
    }

    const config: IExternalGatewayConfig = {
      url,
      apiToken,
    };

    const contextClient = await this.getGatewayClientFromToken(config);
    if (contextClient) {
      config.gatewayClientType = contextClient.type;
      config.gatewayClientId = contextClient.id;
    } else {
      const fallbackGatewayClientId = mode === 'managed'
        ? await this.oneboxRef.managedDcRouter.ensureGatewayClientId()
        : await this.getStoredGatewayClientId();
      if (fallbackGatewayClientId) {
        config.gatewayClientType = 'onebox';
        config.gatewayClientId = fallbackGatewayClientId;
      }
    }

    if (options.requireTarget !== false) {
      if (mode === 'managed') {
        const target = this.oneboxRef.managedDcRouter.getRouteTarget();
        config.targetHost = target.host;
        config.targetPort = target.port;
      } else {
        config.targetHost = (await this.database.getSetting('dcrouterTargetHost'))
          || (await this.database.getSetting('serverIP'))
          || undefined;
        const targetPort = this.parsePort(
          (await this.database.getSetting('dcrouterTargetPort'))
            || (await this.database.getSetting('httpPort'))
            || '80',
        );
        config.targetPort = targetPort;
      }

      if (!config.targetHost) {
        throw new Error('dcrouterTargetHost or serverIP must be configured for external gateway route sync');
      }
    }

    return config;
  }

  private async getMode(): Promise<TDcRouterMode> {
    return await this.oneboxRef.managedDcRouter?.getMode?.() || 'external';
  }

  private async requireConfig(options: { requireTarget?: boolean } = {}): Promise<IExternalGatewayConfig> {
    const config = await this.getConfig(options);
    if (!config) {
      throw new Error('External dcrouter gateway is not configured');
    }
    return config;
  }

  private normalizeUrl(url: string): string {
    const trimmedUrl = url.trim().replace(/\/+$/, '');
    if (!trimmedUrl) return '';
    if (/^https?:\/\//.test(trimmedUrl)) return trimmedUrl;
    return `https://${trimmedUrl}`;
  }

  private parsePort(portValue: string): number {
    const port = Number(portValue);
    if (!Number.isInteger(port) || port < 1 || port > 65535) {
      throw new Error(`Invalid dcrouter target port: ${portValue}`);
    }
    return port;
  }

  private async getStoredGatewayClientId(): Promise<string> {
    return (await this.database.getSetting('dcrouterGatewayClientId')) || '';
  }

  private async getGatewayClientFromToken(config: IExternalGatewayConfig): Promise<{ type: 'onebox'; id: string } | null> {
    try {
      const response = await this.fireDcRouterRequest<
        plugins.servezoneInterfaces.requests.gateway.IReq_GetGatewayClientContext
      >('getGatewayClientContext', {}, config);
      const gatewayClient = response.context.gatewayClient;
      if (!gatewayClient) return null;
      if (gatewayClient.type !== 'onebox') {
        throw new Error(`dcrouter token is bound to unsupported gateway client type: ${gatewayClient.type}`);
      }
      return { type: gatewayClient.type, id: gatewayClient.id };
    } catch (error) {
      logger.debug(`dcrouter gateway client context unavailable: ${getErrorMessage(error)}`);
      return null;
    }
  }

  private buildGatewayClientOwnership(
    service: { id?: number; name?: string },
    hostname: string,
    config: IExternalGatewayConfig,
  ): plugins.servezoneInterfaces.data.IGatewayClientOwnership {
    const ownership: plugins.servezoneInterfaces.data.IGatewayClientOwnership = {
      gatewayClientType: config.gatewayClientType || 'onebox',
      appId: service.name || `service-${service.id}`,
      hostname,
    };
    if (config.gatewayClientId) {
      ownership.gatewayClientId = config.gatewayClientId;
    }
    return ownership;
  }

  private buildGatewayClientRouteRefOwnership(
    service: { id?: number; name?: string },
    routeRef: string,
    config: IExternalGatewayConfig,
  ): plugins.servezoneInterfaces.data.IGatewayClientOwnership {
    const ownership: plugins.servezoneInterfaces.data.IGatewayClientOwnership = {
      gatewayClientType: config.gatewayClientType || 'onebox',
      appId: service.name || `service-${service.id}`,
      routeRef,
    };
    if (config.gatewayClientId) {
      ownership.gatewayClientId = config.gatewayClientId;
    }
    return ownership;
  }

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

  private publicRouteOwnershipKey(appIdArg: string | undefined, routeRefArg: string): string {
    return `${appIdArg || ''}\n${routeRefArg}`;
  }

  private isStalePublicRoute(
    routeArg: TGatewayClientRouteView,
    activePublicRouteOwnershipKeysArg: Set<string>,
  ): boolean {
    const routeRef = routeArg.ownership.routeRef;
    if (!routeRef?.startsWith('public:')) return false;
    return !activePublicRouteOwnershipKeysArg.has(
      this.publicRouteOwnershipKey(routeArg.ownership.appId, routeRef),
    );
  }

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

  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: [],
    }, { allowLegacyDomainPort: false });
  }

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

  private async getAdminUiRoute(): Promise<TExternalGatewayRoute | null> {
    const domain = normalizeHostname((await this.database.getSetting('adminUiDomain')) || '');
    if (!domain) return null;
    return {
      id: 0,
      name: adminUiRouteName,
      domain,
      status: 'running',
    };
  }

  private isAdminUiRecord(record: TGatewayDnsRecordView): boolean {
    const ownerName = record.serviceName || record.appId;
    return ownerName === adminUiRouteName || ownerName === 'onebox';
  }

  private async shouldPreserveUnconfiguredAdminUiRecord(record: TGatewayDnsRecordView): Promise<boolean> {
    return await this.database.getSetting('adminUiDomain') === null && this.isAdminUiRecord(record);
  }

  private buildRoute(
    route: TExternalGatewayRoute,
    config: IExternalGatewayConfig,
  ): plugins.servezoneInterfaces.data.IGatewayRouteConfig {
    return {
      name: this.routeName(route.domain),
      match: {
        ports: [443],
        domains: [route.domain],
      },
      action: {
        type: 'forward',
        targets: [{ host: config.targetHost!, port: config.targetPort! }],
        tls: {
          mode: 'terminate',
          certificate: 'auto',
        },
        websocket: {
          enabled: true,
        },
      },
    };
  }

  private routeName(domain: string): string {
    return `onebox-${domain.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '')}`;
  }

  private buildManageUrl(config: IExternalGatewayConfig, managePath?: string): string {
    const normalizedPath = managePath?.startsWith('/') ? managePath : managePath ? `/${managePath}` : '';
    return `${config.url}${normalizedPath}`;
  }

  private async fireDcRouterRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
    method: T['method'],
    requestData: Omit<T['request'], 'apiToken' | 'identity'>,
    config: IExternalGatewayConfig,
  ): Promise<T['response']> {
    const typedRequest = new plugins.typedrequest.TypedRequest<T>(
      `${config.url}/typedrequest`,
      method,
    );
    return await typedRequest.fire({
      ...requestData,
      apiToken: config.apiToken,
    } as T['request']);
  }
}
