/**
 * Main Onebox coordinator class
 *
 * Coordinates all components and provides the main API
 */

import { logger } from '../logging.js';
import { projectInfo } from '../info.js';
import { getErrorMessage } from '../utils/error.js';
import { normalizePasskeyHostname } from '../utils/domain.js';
import { OneboxDatabase } from '../database/index.js';
import { OneboxDockerManager } from './docker.js';
import { OneboxServicesManager } from './services.js';
import { OneboxRegistriesManager } from './registries.js';
import { OneboxReverseProxy } from './reverseproxy.js';
import { OneboxDnsManager } from './dns.js';
import { OneboxSslManager } from './ssl.js';
import { OneboxDaemon } from './daemon.js';
import { OneboxSystemd } from './systemd.js';
import { CloudflareDomainSync } from './cloudflare-sync.js';
import { CertRequirementManager } from './cert-requirement-manager.js';
import { RegistryManager } from './registry.js';
import { PlatformServicesManager } from './platform-services/index.js';
import { AppStoreManager } from './appstore.js';
import { ProxyLogReceiver } from './proxy-log-receiver.js';
import { BackupManager } from './backup-manager.js';
import { BackupScheduler } from './backup-scheduler.js';
import { ExternalGatewayManager } from './external-gateway.js';
import { ManagedDcRouterManager } from './managed-dcrouter.js';
import { OneboxUpdateManager } from './update-manager.js';
import { OpsServer } from '../opsserver/index.js';
import type * as interfaces from '../../ts_interfaces/index.js';

export class Onebox {
  public database: OneboxDatabase;
  public docker: OneboxDockerManager;
  public services: OneboxServicesManager;
  public registries: OneboxRegistriesManager;
  public reverseProxy: OneboxReverseProxy;
  public dns: OneboxDnsManager;
  public ssl: OneboxSslManager;
  public daemon: OneboxDaemon;
  public systemd: OneboxSystemd;
  public cloudflareDomainSync: CloudflareDomainSync;
  public certRequirementManager: CertRequirementManager;
  public registry: RegistryManager;
  public platformServices: PlatformServicesManager;
  public appStore: AppStoreManager;
  public proxyLogReceiver: ProxyLogReceiver;
  public backupManager: BackupManager;
  public backupScheduler: BackupScheduler;
  public managedDcRouter: ManagedDcRouterManager;
  public externalGateway: ExternalGatewayManager;
  public updateManager: OneboxUpdateManager;
  public opsServer: OpsServer;

  private initialized = false;

  constructor() {
    // Initialize database first
    this.database = new OneboxDatabase();

    // Initialize managers (passing reference to main Onebox instance)
    this.docker = new OneboxDockerManager();
    this.services = new OneboxServicesManager(this);
    this.registries = new OneboxRegistriesManager(this);
    this.reverseProxy = new OneboxReverseProxy(this);
    this.dns = new OneboxDnsManager(this);
    this.ssl = new OneboxSslManager(this);
    this.daemon = new OneboxDaemon(this);
    this.systemd = new OneboxSystemd();
    this.registry = new RegistryManager({
      dataDir: './.nogit/registry-data',
      port: 4000,
      baseUrl: 'localhost:3000',
    });

    // Initialize domain management
    this.cloudflareDomainSync = new CloudflareDomainSync(this.database);
    this.certRequirementManager = new CertRequirementManager(this.database, this.ssl);

    // Initialize platform services manager
    this.platformServices = new PlatformServicesManager(this);

    // Initialize App Store manager
    this.appStore = new AppStoreManager(this);

    // Initialize reverse proxy log receiver
    this.proxyLogReceiver = new ProxyLogReceiver(9999);

    // Initialize Backup manager
    this.backupManager = new BackupManager(this);

    // Initialize Backup scheduler
    this.backupScheduler = new BackupScheduler(this);

    // Initialize optional dcrouter gateway integration
    this.managedDcRouter = new ManagedDcRouterManager(this);
    this.externalGateway = new ExternalGatewayManager(this);
    this.updateManager = new OneboxUpdateManager();

    // Initialize OpsServer (TypedRequest-based server)
    this.opsServer = new OpsServer(this);
  }

  /**
   * Initialize all components
   */
  async init(): Promise<void> {
    try {
      logger.info('Initializing Onebox...');

      // Initialize database
      await this.database.init();

      await this.ensurePasskeyConfiguration();
      await this.ensurePasskeyAdminUser();

      // Initialize Docker
      await this.docker.init();

      try {
        await this.managedDcRouter.prepareGatewaySettings();
      } catch (error) {
        logger.warn(`Managed dcrouter settings preparation failed: ${getErrorMessage(error)}`);
      }

      if (await this.managedDcRouter.getMode() !== 'managed') {
        try {
          await this.managedDcRouter.stop();
        } catch (error) {
          logger.warn(`Failed to stop inactive managed dcrouter: ${getErrorMessage(error)}`);
        }
      }

      // Start proxy log receiver before reverse proxy startup.
      try {
        await this.proxyLogReceiver.start();
      } catch (error) {
        logger.warn(`Failed to start proxy log receiver: ${getErrorMessage(error)}`);
      }

      // Initialize Reverse Proxy
      await this.reverseProxy.init();

      // Load routes and certificates for reverse proxy
      await this.reverseProxy.reloadRoutes();
      await this.reverseProxy.reloadCertificates();

      // Start HTTP reverse proxy (non-critical - don't fail init if ports are busy)
      // Use 8080/8443 in dev mode to avoid permission issues
      const isDev = process.env.ONEBOX_DEV === 'true' || process.argv.includes('--ephemeral');
      const isManagedDcRouter = await this.managedDcRouter.getMode() === 'managed';
      const httpPort = isDev || isManagedDcRouter ? 8080 : 80;
      const httpsPort = isDev || isManagedDcRouter ? 8443 : 443;

      try {
        await this.reverseProxy.startHttp(httpPort);
      } catch (error) {
        logger.warn(`Failed to start HTTP reverse proxy: ${getErrorMessage(error)}`);
      }

      // Start HTTPS reverse proxy if certificates are available
      try {
        await this.reverseProxy.startHttps(httpsPort);
      } catch (error) {
        logger.warn(`Failed to start HTTPS reverse proxy: ${getErrorMessage(error)}`);
      }

      // Initialize DNS (non-critical)
      try {
        await this.dns.init();
      } catch (error) {
        logger.warn('DNS initialization failed - DNS features will be disabled');
      }

      // Initialize SSL (non-critical)
      try {
        await this.ssl.init();
      } catch (error) {
        logger.warn('SSL initialization failed - SSL features will be limited');
      }

      // Initialize Cloudflare domain sync (non-critical)
      try {
        await this.cloudflareDomainSync.init();
      } catch (error) {
        logger.warn('Cloudflare domain sync initialization failed - domain sync will be limited');
      }

      // Initialize managed local dcrouter before syncing delegated routes.
      try {
        await this.managedDcRouter.init();
      } catch (error) {
        logger.warn('Managed dcrouter initialization failed - local gateway sync will be disabled');
        logger.warn(`Error: ${getErrorMessage(error)}`);
      }

      // Initialize external dcrouter gateway (non-critical)
      try {
        await this.externalGateway.init();
      } catch (error) {
        logger.warn('External dcrouter gateway initialization failed - edge sync will be disabled');
        logger.warn(`Error: ${getErrorMessage(error)}`);
      }

      // Initialize Onebox Registry (non-critical)
      try {
        await this.registry.init();
      } catch (error) {
        logger.warn('Onebox Registry initialization failed - local registry will be disabled');
        logger.warn(`Error: ${getErrorMessage(error)}`);
      }

      // Initialize platform services and reconcile legacy plaintext credentials before API exposure.
      try {
        await this.platformServices.init();
      } catch (error) {
        throw new Error(`Platform services initialization failed: ${getErrorMessage(error)}`, {
          cause: error,
        });
      }

      // Initialize App Store (non-critical)
      try {
        await this.appStore.init();
      } catch (error) {
        logger.warn('App Store initialization failed - app templates will be unavailable until reconnected');
        logger.warn(`Error: ${getErrorMessage(error)}`);
      }

      // Login to all registries
      await this.registries.loginToAllRegistries();

      // Start auto-update monitoring for registry services
      this.services.startAutoUpdateMonitoring();

      // Initialize BackupManager (containerarchive repository, non-critical)
      try {
        await this.backupManager.init();
      } catch (error) {
        logger.warn('BackupManager initialization failed - backups will be limited');
        logger.warn(`Error: ${getErrorMessage(error)}`);
      }

      // Initialize Backup Scheduler (non-critical)
      try {
        await this.backupScheduler.init();
      } catch (error) {
        logger.warn('Backup scheduler initialization failed - scheduled backups will be disabled');
        logger.warn(`Error: ${getErrorMessage(error)}`);
      }

      this.initialized = true;
      logger.success('Onebox initialized successfully');
    } catch (error) {
      logger.error(`Failed to initialize Onebox: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  public async assertAdminUiDomainCompatible(domainArg: string): Promise<void> {
    const adminUsername = process.env.ONEBOX_ADMIN_USERNAME || 'admin';
    const adminUser = await this.database.getUserByUsername(adminUsername);
    if (!adminUser) {
      return;
    }

    const credentials = adminUser.passkeyCredentials;
    if (!Array.isArray(credentials)) {
      throw new Error('Onebox administrator passkey data is malformed');
    }

    const firstCredential = credentials[0];
    if (!firstCredential) {
      return;
    }

    if (
      credentials.some(
        (credentialArg) =>
          credentialArg.rpId !== firstCredential.rpId
          || credentialArg.origin !== firstCredential.origin,
      )
    ) {
      throw new Error('Onebox passkey relying-party data is inconsistent');
    }

    if (domainArg !== firstCredential.rpId) {
      throw new Error(
        `Admin UI domain is pinned to ${firstCredential.rpId} by the registered passkey`,
      );
    }
  }

  private async ensurePasskeyConfiguration(): Promise<void> {
    const storedDomain = await this.database.getSetting('adminUiDomain');
    const normalizedStoredDomain = storedDomain
      ? normalizePasskeyHostname(storedDomain)
      : null;
    if (storedDomain && !normalizedStoredDomain) {
      throw new Error(
        'Configured adminUiDomain must be a hostname served over HTTPS on port 443',
      );
    }

    const environmentDomain = process.env.ONEBOX_ADMIN_UI_DOMAIN?.trim();
    if (!environmentDomain) {
      return;
    }

    const normalizedEnvironmentDomain = normalizePasskeyHostname(environmentDomain);
    if (!normalizedEnvironmentDomain) {
      throw new Error(
        'ONEBOX_ADMIN_UI_DOMAIN must be a hostname served over HTTPS on port 443',
      );
    }
    if (
      normalizedStoredDomain
      && normalizedStoredDomain !== normalizedEnvironmentDomain
    ) {
      throw new Error(
        'ONEBOX_ADMIN_UI_DOMAIN does not match the persisted adminUiDomain setting',
      );
    }
    if (!normalizedStoredDomain) {
      await this.database.setSetting('adminUiDomain', normalizedEnvironmentDomain);
    }
  }

  private async ensurePasskeyAdminUser(): Promise<void> {
    const adminUsername = process.env.ONEBOX_ADMIN_USERNAME || 'admin';
    const webauthnUserIdBytes = new Uint8Array(32);
    crypto.getRandomValues(webauthnUserIdBytes);
    const webauthnUserId = Buffer.from(webauthnUserIdBytes).toString('base64url');
    await this.database.ensurePasskeyAdminUser(adminUsername, webauthnUserId);
  }

  /**
   * Check if Onebox is initialized
   */
  isInitialized(): boolean {
    return this.initialized;
  }

  /**
   * Get system status
   */
  async getSystemStatus() {
    try {
      const dockerRunning = await this.docker.isDockerRunning();
      const proxyStatus = await this.reverseProxy.observeStatus();
      const dnsConfigured = this.dns.isConfigured();
      const sslConfigured = this.ssl.isConfigured();
      const oneboxUpdate = await this.updateManager.getUpdateStatus();

      const services = await this.services.listServices();
      const runningServices = services.filter((s) => s.status === 'running').length;
      const totalServices = services.length;

      // Get platform services status with resource counts
      const platformServices = await this.platformServices.getAllPlatformServices();
      const providers = this.platformServices.getAllProviders();
      const platformServicesStatus: interfaces.data.ISystemStatus['platformServices'] = [];
      for (const provider of providers) {
        const service = platformServices.find((s) => s.type === provider.type);
        // For CoreTraffic, check actual runtime status since it starts without a DB record
        let status: interfaces.data.TPlatformServiceStatus = service?.status || 'not-deployed';
        if (provider.type === 'coretraffic') {
          status = proxyStatus.state;
        }
        // Count resources for this platform service
        const resourceCount = service?.id
          ? (await this.database.getPlatformResourcesByPlatformService(service.id)).length
          : 0;
        platformServicesStatus.push({
          type: provider.type,
          displayName: provider.displayName,
          status,
          resourceCount,
        });
      }

      // Get certificate health summary
      const certificates = await this.ssl.listCertificates();
      const now = Date.now();
      const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
      let validCount = 0;
      let expiringCount = 0;
      let expiredCount = 0;
      const expiringDomains: { domain: string; daysRemaining: number }[] = [];

      for (const cert of certificates) {
        if (cert.expiryDate <= now) {
          expiredCount++;
        } else if (cert.expiryDate <= now + thirtyDaysMs) {
          expiringCount++;
          const daysRemaining = Math.floor((cert.expiryDate - now) / (24 * 60 * 60 * 1000));
          expiringDomains.push({ domain: cert.domain, daysRemaining });
        } else {
          validCount++;
        }
      }

      // Sort expiring domains by days remaining (ascending)
      expiringDomains.sort((a, b) => a.daysRemaining - b.daysRemaining);

      // Aggregate resource usage across all running service containers
      let totalCpu = 0;
      let totalMemoryUsed = 0;
      let totalMemoryLimit = 0;
      let totalNetworkIn = 0;
      let totalNetworkOut = 0;

      if (dockerRunning) {
        const allServices = await this.services.listServices();
        const runningUserServices = allServices.filter((s) => s.status === 'running' && s.containerID);
        logger.debug(`Resource stats: ${runningUserServices.length} running user services`);

        const statsPromises = runningUserServices
          .map((s) => {
            logger.debug(`Fetching stats for user service: ${s.name} (${s.containerID})`);
            return this.docker.getContainerStats(s.containerID!).catch((err) => {
              logger.debug(`Stats failed for ${s.name}: ${(err as Error).message}`);
              return null;
            });
          });

        // Also get stats for platform service containers
        const allPlatformServices = await this.platformServices.getAllPlatformServices();
        const runningPlatformServices = allPlatformServices.filter((s) => s.status === 'running' && s.containerId);
        logger.debug(`Resource stats: ${runningPlatformServices.length} running platform services`);

        const platformStatsPromises = runningPlatformServices
          .map((s) => {
            logger.debug(`Fetching stats for platform service: ${s.type} (${s.containerId})`);
            return this.docker.getContainerStats(s.containerId!).catch((err) => {
              logger.debug(`Stats failed for ${s.type}: ${(err as Error).message}`);
              return null;
            });
          });

        const allStats = await Promise.all([...statsPromises, ...platformStatsPromises]);
        let successCount = 0;
        for (const stats of allStats) {
          if (stats) {
            successCount++;
            totalCpu += stats.cpuPercent;
            totalMemoryUsed += stats.memoryUsed;
            totalMemoryLimit = Math.max(totalMemoryLimit, stats.memoryLimit);
            totalNetworkIn += stats.networkRx;
            totalNetworkOut += stats.networkTx;
          }
        }
        logger.debug(`Resource stats: ${successCount}/${allStats.length} containers returned stats. CPU: ${totalCpu}, Mem: ${totalMemoryUsed}`);
      }

      return {
        onebox: {
          version: projectInfo.version,
          update: oneboxUpdate,
        },
        docker: {
          running: dockerRunning,
          version: dockerRunning ? await this.docker.getDockerVersion() : null,
          cpuUsage: Math.round(totalCpu * 10) / 10,
          memoryUsage: totalMemoryUsed,
          memoryTotal: totalMemoryLimit,
          networkIn: totalNetworkIn,
          networkOut: totalNetworkOut,
        },
        reverseProxy: proxyStatus,
        dns: {
          configured: dnsConfigured,
        },
        ssl: {
          configured: sslConfigured,
          certificateCount: certificates.length,
        },
        services: {
          total: totalServices,
          running: runningServices,
          stopped: totalServices - runningServices,
        },
        platformServices: platformServicesStatus,
        certificateHealth: {
          valid: validCount,
          expiringSoon: expiringCount,
          expired: expiredCount,
          expiringDomains: expiringDomains.slice(0, 5), // Top 5 expiring
        },
      };
    } catch (error) {
      logger.error(`Failed to get system status: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Start OpsServer (TypedRequest-based, serves new UI)
   */
  async startHttpServer(port?: number): Promise<void> {
    await this.opsServer.start(port || 3000);
  }

  /**
   * Stop OpsServer
   */
  async stopHttpServer(): Promise<void> {
    await this.opsServer.stop();
  }

  /**
   * Shutdown Onebox gracefully
   */
  async shutdown(): Promise<void> {
    try {
      logger.info('Shutting down Onebox...');

      // Stop auto-update monitoring
      this.services.stopAutoUpdateMonitoring();

      // Stop backup scheduler
      await this.backupScheduler.stop();

      // Stop daemon if running
      await this.daemon.stop();

      // Stop OpsServer if running
      await this.opsServer.stop();

      // Stop reverse proxy if running
      await this.reverseProxy.stop();

      // Stop proxy log receiver
      await this.proxyLogReceiver.stop();

      // Stop built-in registry and backing smartstorage server
      await this.registry.stop();

      // Close backup archive
      await this.backupManager.close();

      // Release Docker client resources after all Docker-backed managers stopped.
      await this.docker.stop();

      // Close database
      await this.database.close();

      logger.success('Onebox shutdown complete');
    } catch (error) {
      logger.error(`Error during shutdown: ${getErrorMessage(error)}`);
    }
  }
}
