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

const adminUiRouteName = 'onebox-admin-ui';

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

interface IExternalGatewayConfig {
  url: string;
  apiToken: string;
  gatewayClientType?: TWorkHosterType;
  gatewayClientId?: string;
  /** @deprecated Use gatewayClientId. */
  workHosterId?: string;
  targetHost?: string;
  targetPort?: number;
}

interface IGatewayClientContextResponse {
  context: {
    role: 'admin' | 'gatewayClient' | 'operator';
    gatewayClient?: {
      type: 'onebox' | 'cloudly' | 'custom';
      id: string;
    };
  };
}

interface IWorkHosterDomain {
  id?: string;
  name: string;
  source?: 'dcrouter' | 'provider';
  authoritative?: boolean;
  providerId?: string;
  serviceCount?: number;
  managePath?: string;
  manageUrl?: string;
  capabilities?: {
    canCreateSubdomains: boolean;
    canManageDnsRecords: boolean;
    canIssueCertificates: boolean;
    canHostEmail: boolean;
  };
}

interface IGatewayDnsRecord {
  id: string;
  domainId: string;
  domainName?: string;
  name: string;
  type: string;
  value: string;
  ttl: number;
  source: string;
  status: 'active' | 'missing';
  gatewayClientType: 'onebox' | 'cloudly' | 'custom';
  gatewayClientId: string;
  appId: string;
  hostname: string;
  routeId?: string;
  serviceName?: string;
  managePath?: string;
  manageUrl?: string;
}

interface IWorkAppRouteOwnership {
  workHosterType: TWorkHosterType;
  workHosterId: string;
  workAppId: string;
  hostname: string;
}

interface IGatewayClientOwnership {
  gatewayClientType?: TWorkHosterType;
  gatewayClientId?: string;
  appId: string;
  hostname: string;
}

interface IWorkAppRouteSyncResult {
  success: boolean;
  action?: 'created' | 'updated' | 'deleted' | 'unchanged';
  routeId?: string;
  message?: string;
}

interface IDcRouterCertificateExport {
  success: boolean;
  cert?: {
    id: string;
    domainName: string;
    created: number;
    validUntil: number;
    privateKey: string;
    publicKey: string;
    csr: string;
  };
  message?: string;
}

interface IDcRouterRouteConfig {
  name: string;
  match: {
    ports: number[];
    domains: string[];
  };
  action: {
    type: 'forward';
    targets: Array<{ host: string; port: number }>;
    tls: {
      mode: 'terminate';
      certificate: 'auto';
    };
    websocket: {
      enabled: boolean;
    };
  };
}

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 = this.getAdminUiRoute();
    const adminUiDomain = adminUiRoute?.domain;
    const services = this.database.getAllServices()
      .filter((service) =>
        service.domain && service.status === 'running' && service.domain !== adminUiDomain
      );
    const activeHostnames = new Set(services.map((service) => service.domain!));

    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 {
        await this.syncServiceRoute(service);
      } catch (error) {
        logger.warn(
          `Failed to sync external gateway route for ${service.domain}: ${getErrorMessage(error)}`,
        );
      }
    }

    await this.deleteStaleServiceRoutes(activeHostnames);
  }

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

    for (const record of records) {
      if (!record.hostname || activeHostnamesArg.has(record.hostname)) continue;
      if (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)
          }`,
        );
      }
    }
  }

  public async isConfigured(): Promise<boolean> {
    if (this.getMode() === 'disabled') {
      return false;
    }
    const mode = this.getMode();
    const url = mode === 'managed'
      ? this.oneboxRef.managedDcRouter.getGatewayUrl()
      : this.normalizeUrl(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 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 = this.database.getDomainByName(domainName);
      const defaultWildcard = gatewayDomain.capabilities?.canIssueCertificates !== false;

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

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

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

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

    try {
      const response = await this.fireDcRouterRequest<{ domains: IWorkHosterDomain[] }>(
        'getGatewayClientDomains',
        config.gatewayClientId ? { gatewayClientId: config.gatewayClientId } : {},
        config,
      );
      return response.domains.map((domain) => ({
        ...domain,
        manageUrl: this.buildManageUrl(config, domain.managePath),
      }));
    } catch (error) {
      logger.debug(`Falling back to legacy gateway domain API: ${getErrorMessage(error)}`);
      const response = await this.fireDcRouterRequest<{ domains: IWorkHosterDomain[] }>(
        'getWorkHosterDomains',
        {},
        config,
      );
      return response.domains.map((domain) => ({
        ...domain,
        manageUrl: this.buildManageUrl(config, domain.managePath),
      }));
    }
  }

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

    try {
      const response = await this.fireDcRouterRequest<{ records: IGatewayDnsRecord[] }>(
        '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 syncServiceRoute(service: IService): Promise<void> {
    if (!service.domain) return;

    await this.syncGatewayRoute({
      id: service.id,
      name: service.name,
      domain: service.domain,
      status: service.status,
    });
  }

  public async syncAdminUiRoute(): Promise<void> {
    const route = 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<IWorkAppRouteSyncResult>(
      'syncGatewayClientRoute',
      {
        ownership: this.buildGatewayClientOwnership(route, route.domain, config),
        route: this.buildRoute(route, config),
        enabled: route.status === 'running',
      },
      config,
    ).catch(async () => {
      return await this.fireDcRouterRequest<IWorkAppRouteSyncResult>(
        'syncWorkAppRoute',
        {
          ownership: this.buildOwnership(route, route.domain, config),
          route: this.buildRoute(route, config),
          enabled: route.status === 'running',
        },
        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: Pick<IService, 'id' | 'name' | 'domain'>,
  ): Promise<void> {
    if (!service.domain) return;

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

    const result = await this.fireDcRouterRequest<IWorkAppRouteSyncResult>(
      'syncGatewayClientRoute',
      {
        ownership: this.buildGatewayClientOwnership(service, service.domain, config),
        delete: true,
      },
      config,
    ).catch(async () => {
      return await this.fireDcRouterRequest<IWorkAppRouteSyncResult>(
        'syncWorkAppRoute',
        {
          ownership: this.buildOwnership(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<IDcRouterCertificateExport>(
      'exportCertificate',
      { domain },
      config,
    );

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

    const now = Date.now();
    const existingCertificate = this.database.getSSLCertificate(domain);
    if (existingCertificate) {
      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 = this.getMode();
    if (mode === 'disabled') {
      return null;
    }

    const url = mode === 'managed'
      ? this.oneboxRef.managedDcRouter.getGatewayUrl()
      : this.normalizeUrl(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;
      config.workHosterId = contextClient.id;
    } else {
      const fallbackGatewayClientId = mode === 'managed'
        ? this.oneboxRef.managedDcRouter.ensureGatewayClientId()
        : this.getStoredGatewayClientId();
      if (fallbackGatewayClientId) {
        config.gatewayClientType = 'onebox';
        config.gatewayClientId = fallbackGatewayClientId;
        config.workHosterId = 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 = this.database.getSetting('dcrouterTargetHost')
          || this.database.getSetting('serverIP')
          || undefined;
        const targetPort = this.parsePort(
          this.database.getSetting('dcrouterTargetPort')
            || 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 getMode(): TDcRouterMode {
    return 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 getStoredGatewayClientId(): string {
    return this.database.getSetting('dcrouterGatewayClientId') || this.database.getSetting('dcrouterWorkHosterId') || '';
  }

  private async getGatewayClientFromToken(config: IExternalGatewayConfig): Promise<{ type: TWorkHosterType; id: string } | null> {
    try {
      const response = await this.fireDcRouterRequest<IGatewayClientContextResponse>(
        '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 buildOwnership(
    service: Pick<IService, 'id' | 'name'>,
    hostname: string,
    config: IExternalGatewayConfig,
  ): IWorkAppRouteOwnership {
    return {
      workHosterType: 'onebox',
      workHosterId: config.gatewayClientId || '',
      workAppId: service.name || `service-${service.id}`,
      hostname,
    };
  }

  private buildGatewayClientOwnership(
    service: Pick<IService, 'id' | 'name'>,
    hostname: string,
    config: IExternalGatewayConfig,
  ): IGatewayClientOwnership {
    const ownership: IGatewayClientOwnership = {
      gatewayClientType: config.gatewayClientType || 'onebox',
      appId: service.name || `service-${service.id}`,
      hostname,
    };
    if (config.gatewayClientId) {
      ownership.gatewayClientId = config.gatewayClientId;
    }
    return ownership;
  }

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

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

  private shouldPreserveUnconfiguredAdminUiRecord(record: IGatewayDnsRecord): boolean {
    return this.database.getSetting('adminUiDomain') === null && this.isAdminUiRecord(record);
  }

  private buildRoute(
    route: TExternalGatewayRoute,
    config: IExternalGatewayConfig,
  ): IDcRouterRouteConfig {
    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<TResponse>(
    method: string,
    requestData: Record<string, unknown>,
    config: IExternalGatewayConfig,
  ): Promise<TResponse> {
    const typedRequest = new plugins.typedrequest.TypedRequest<any>(
      `${config.url}/typedrequest`,
      method,
    );
    return await typedRequest.fire({
      ...requestData,
      apiToken: config.apiToken,
    }) as TResponse;
  }
}
