import type { Server } from 'http';
import crypto from 'crypto';
import os from 'os';
import { FileSystemService } from '../services/FileSystemService';
import { NodeRegistry } from '../services/NodeRegistry';
import { DatabaseService } from '../services/DatabaseService';
import { LicenseService } from '../services/LicenseService';
import SelfUpdateService from '../services/SelfUpdateService';
import { ImageOperationService } from '../services/ImageOperationService';
import SelfIdentityService from '../services/SelfIdentityService';
import { MonitorService } from '../services/MonitorService';
import { AutoHealService } from '../services/AutoHealService';
import { HealthGateService } from '../services/HealthGateService';
import { ServiceUpdateRecoveryService } from '../services/ServiceUpdateRecoveryService';
import { DeployedStackDeletionService } from '../services/DeployedStackDeletionService';
import { StackUpdateRecoveryService } from '../services/StackUpdateRecoveryService';
import { FleetSyncRetryService } from '../services/FleetSyncRetryService';
import { SuppressionRetractionRetryService } from '../services/SuppressionRetractionRetryService';
import { DockerEventManager } from '../services/DockerEventManager';
import TrivyService, { sweepStaleTrivyTempDirs } from '../services/TrivyService';
import { ImageUpdateService } from '../services/ImageUpdateService';
import { SchedulerService } from '../services/SchedulerService';
import { MfaService } from '../services/MfaService';
import { MeshService } from '../services/MeshService';
import { BlueprintReconciler } from '../services/BlueprintReconciler';
import { CveIntelService } from '../services/CveIntelService';
import { applyPilotModeCapabilityFilter } from '../services/CapabilityRegistry';
import { PilotTunnelManager } from '../services/PilotTunnelManager';
import { PilotMetrics } from '../services/PilotMetrics';
import { invalidateRemoteMetaCache } from '../helpers/cacheInvalidation';
import { sweepStaleTempDirs as sweepStaleGitTempDirs, sweepGitManifestOrphans } from '../services/GitSourceService';
import { assertCreatesSettled, reclassifyInterruptedOperations, resolveInterruptedCreates } from '../services/gitops/createRecovery';
import { loadMigrationManifests, migrateDirectGitStacks, migrateInlineBlueprints } from '../services/gitops/migrate';
import { setGitOpsEventSink } from '../services/gitops/publish';
import { NotificationService } from '../services/NotificationService';
import { sanitizeForLog } from '../utils/safeLog';
import { PORT } from '../helpers/constants';
import { sweepDockerAuthTempDirs, classifyDockerAuthChildName } from '../helpers/dockerAuthTempDir';
import { recordRegistryDeliveryEvent } from '../helpers/registryDeliveryEvidence';
import { PreparedSourceStore } from '../services/preparedSourceStore';
import { RegistryDeliveryService } from '../services/RegistryDeliveryService';
import { RegistryDeliveryReconciler } from '../services/RegistryDeliveryReconciler';
import { LOW_MEMORY_FLOOR_BYTES } from '../utils/spawnErrors';

function isPilotMode(): boolean {
  return process.env.SENCHO_MODE === 'pilot';
}

/**
 * Pilot-agent hosts never run the first-run setup wizard, so the wizard
 * path that normally generates `auth_jwt_secret` (routes/auth.ts) never
 * fires. Without that secret, the agent-side loopback auth helper
 * (`pilot/agent.ts::getLoopbackAuthHeader`) cannot mint the
 * `pilot_tunnel`-scoped JWT it injects on every forwarded HTTP/WS request,
 * and the local Sencho's `authMiddleware` rejects every proxied call with
 * 401 "Authentication required". Generate the secret here on first boot in
 * pilot mode; subsequent boots reuse the persisted value. No-op outside
 * pilot mode (the wizard owns the lifecycle there).
 *
 * Returns true when a fresh secret was written, false otherwise.
 */
export function ensurePilotJwtSecret(): boolean {
  if (!isPilotMode()) return false;
  const dbSvc = DatabaseService.getInstance();
  if (dbSvc.getGlobalSettings().auth_jwt_secret) return false;
  const generated = crypto.randomBytes(64).toString('hex');
  dbSvc.updateGlobalSetting('auth_jwt_secret', generated);
  console.log('[Startup] pilot-agent: generated local auth_jwt_secret');
  return true;
}

/** Keep the pilot's persisted local node aligned with its configured mount. */
export function reconcilePilotComposeDir(): boolean {
  if (!isPilotMode()) return false;
  const configuredDir = process.env.COMPOSE_DIR?.trim();
  if (!configuredDir) return false;

  const db = DatabaseService.getInstance();
  const localNode = db.getDefaultNode();
  if (!localNode || localNode.compose_dir === configuredDir) return false;

  db.updateNode(localNode.id, { compose_dir: configuredDir });
  console.log(`[Startup] pilot-agent: compose directory set to ${configuredDir}`);
  return true;
}

function clearSelfContainerNotificationRouting(): void {
  const identity = SelfIdentityService.getInstance().getIdentity();
  const changed = DatabaseService.getInstance().clearSelfContainerNotificationRouting(
    NodeRegistry.getInstance().getDefaultNodeId(),
    {
      containerName: identity.containerName,
      composeProjectName: identity.composeProjectName,
    },
  );
  if (changed > 0) {
    console.log(`[Startup] Cleared stack routing from ${changed} Sencho self-container notification(s)`);
  }
}

/**
 * Run the startup sequence: stack-directory migration, service initialization,
 * background watchdogs, then bind the HTTP server. The caller passes the
 * already-constructed server so tests can import the module without binding a
 * port.
 */
export async function startServer(server: Server): Promise<void> {
  const freeBytes = os.freemem();
  const freeMiB = Math.round(freeBytes / (1024 * 1024));
  const totalMiB = Math.round(os.totalmem() / (1024 * 1024));
  const floorMiB = Math.round(LOW_MEMORY_FLOOR_BYTES / (1024 * 1024));
  console.log(`[Startup] Host memory: ${freeMiB} MiB free of ${totalMiB} MiB`);
  if (freeBytes < LOW_MEMORY_FLOOR_BYTES) {
    console.warn(
      `[Startup] Free host memory is ${freeMiB} MiB (below ${floorMiB} MiB floor). ` +
      'Sencho operations that spawn child processes (docker, /bin/sh) may fail ' +
      'with misleading ENOENT errors under memory pressure.'
    );
  }

  reconcilePilotComposeDir();

  try {
    console.log('Running stack migration check...');
    const defaultFsService = FileSystemService.getInstance(NodeRegistry.getInstance().getDefaultNodeId());
    await defaultFsService.migrateFlatToDirectory();
    console.log('Migration check completed');
  } catch (error) {
    console.error('Migration failed:', error);
  }

  ensurePilotJwtSecret();

  if (isPilotMode()) {
    applyPilotModeCapabilityFilter();
  }

  // Hydrate pilot/mesh counters from the persisted snapshot before any
  // service that increments them (MeshService, PilotTunnelManager) starts.
  // Failures fall through to zero-initialized counters; do not block boot.
  try {
    PilotMetrics.load(DatabaseService.getInstance());
  } catch (err) {
    console.warn('[Startup] PilotMetrics load failed:', (err as Error).message);
  }

  // Initialize the license service before any tier-gated code can run.
  LicenseService.getInstance().initialize();

  // Announce committed GitOps transitions from here on. This has to precede
  // every reconcile and migration pass below, because all of them write
  // history: the deletion reconcile tombstones applications and targets, and
  // it awaits inside its loop, so a drain would otherwise land while the sink
  // was still absent. Those rows would then be counted and never signalled,
  // and the unannounced warning would fire on every boot that has a prepared
  // deletion intent, which is the fastest way to teach an operator to ignore
  // it when it means something. Nothing is connected this early, so announcing
  // costs nothing; the closure resolves the notification service lazily, so it
  // can be installed as soon as the database is up.
  setGitOpsEventSink((event) => NotificationService.getInstance().broadcastEvent(event));

  // Deletion-intent reconciliation must finish before mutation-capable
  // background services or HTTP accept traffic that could recreate a stack
  // name still covered by a prepared/ready tombstone.
  try {
    await DeployedStackDeletionService.getInstance().reconcileAtStartup();
  } catch (err) {
    console.error('[Startup] Deployed stack deletion reconcile failed:', (err as Error).message);
  }

  // Interrupted rollback restores must finish before mutation-capable services
  // or HTTP accept traffic. Fail closed: rethrow so unresolved intents never
  // leave mutators or HTTP accepting writes.
  try {
    await StackUpdateRecoveryService.getInstance().reconcileInterruptedRestoresAtStartup();
  } catch (err) {
    console.error('[Startup] Stack update recovery restore reconcile failed:', (err as Error).message);
    throw err;
  }
  StackUpdateRecoveryService.getInstance().start();

  // Interrupted creates are settled here, ahead of the background mutators and
  // the HTTP bind below, because a scheduler or webhook that fired first could
  // act on a stack whose ownership is still undecided. Fail closed for the same
  // reason the restore reconcile above does: a create left unresolved can leave
  // a half-built stack directory that the deploy path cannot tell apart from a
  // finished one, and starting anyway would let a mutator act on it.
  try {
    const settled = await resolveInterruptedCreates();
    for (const entry of settled) {
      console.log(`[GitOps] Interrupted create for ${sanitizeForLog(entry.stackName)}: ${entry.outcome}`);
    }
    assertCreatesSettled(settled);
  } catch (err) {
    console.error('[GitOps] Interrupted-create recovery failed:', err instanceof Error ? err.stack ?? err.message : String(err));
    throw err;
  }
  // Operations the last process never finished are reclassified as unknown.
  // Without this an interrupted fetch or apply reports as still running for
  // ever and the stack is offered no actions at all.
  try {
    const reclassified = reclassifyInterruptedOperations();
    if (reclassified > 0) {
      console.log(`[GitOps] Reclassified ${reclassified} interrupted operation(s) as unknown`);
    }
  } catch (err) {
    console.error('[GitOps] Interrupted-operation reclassification failed:', err instanceof Error ? err.stack ?? err.message : String(err));
  }

  // Git stacks that predate the revision state model are brought into it here,
  // after interrupted work is settled so migration never races a half-finished
  // create, and before any mutation service can act on a stack the model does
  // not yet describe.
  try {
    await loadMigrationManifests();
    const migrated = migrateDirectGitStacks().filter((entry) => entry.outcome !== 'skipped_current');
    for (const entry of migrated) {
      console.log(`[GitOps] Migrated Git stack ${sanitizeForLog(entry.stackName)}: ${entry.outcome}`);
    }
  } catch (err) {
    console.error('[GitOps] Migration of pre-existing Git stacks failed:', err instanceof Error ? err.stack ?? err.message : String(err));
  }

  // Blueprints migrate separately, and a failure in one must not stop the
  // other: they share nothing, and coupling them would let a single unreadable
  // Git stack keep every Blueprint outside the model.
  try {
    const migratedBlueprints = migrateInlineBlueprints().filter((entry) => entry.outcome !== 'skipped_current');
    for (const entry of migratedBlueprints) {
      console.log(`[GitOps] Migrated blueprint ${sanitizeForLog(entry.stackName)}: ${entry.outcome}`);
    }
  } catch (err) {
    console.error('[GitOps] Migration of pre-existing blueprints failed:', err instanceof Error ? err.stack ?? err.message : String(err));
  }

  // The managed-area sweep follows. It preserves anything whose ownership it
  // cannot prove, so a failure here can only leave files behind, never remove
  // the wrong ones, and retrying next boot is safe.
  try {
    await sweepGitManifestOrphans();
  } catch (err) {
    console.warn('[GitManifest] Managed-area sweep failed:', err instanceof Error ? err.message : String(err));
  }

  // Registry delivery recovery sweeps must settle before any mutation-capable
  // producer starts (AUD-36).
  try {
    const deliverySourceId = RegistryDeliveryService.getInstance().getDeliverySourceId();
    PreparedSourceStore.getInstance().configure(deliverySourceId);
    const dockerSweep = await sweepDockerAuthTempDirs(deliverySourceId);
    for (const basename of dockerSweep.swept) {
      const kind = classifyDockerAuthChildName(basename);
      recordRegistryDeliveryEvent({
        deliverySourceId,
        eventType: kind === 'delivered' ? 'swept_delivered' : 'swept_local',
        tempDirId: basename,
      });
    }
    for (const basename of dockerSweep.legacyOrphansObserved) {
      recordRegistryDeliveryEvent({
        deliverySourceId,
        eventType: 'legacy_orphan_observed',
        tempDirId: basename,
      });
    }
    const preparedSwept = await PreparedSourceStore.getInstance().sweepOrphans(deliverySourceId);
    for (const basename of preparedSwept) {
      recordRegistryDeliveryEvent({
        deliverySourceId,
        eventType: 'swept_prepared',
        tempDirId: basename,
      });
    }
    PreparedSourceStore.getInstance().start();
    RegistryDeliveryReconciler.getInstance().start();
  } catch (err) {
    console.warn('[RegistryDelivery] Startup sweep failed:', err instanceof Error ? err.message : String(err));
  }

  // Synchronous starts: schedule background timers and continue. None of
  // these fire their first tick for at least a few seconds, so they
  // safely run alongside the async initializers below.
  MonitorService.getInstance().start();
  AutoHealService.getInstance().start();
  HealthGateService.getInstance().start();
  ServiceUpdateRecoveryService.getInstance().start();
  FleetSyncRetryService.getInstance().start();
  SuppressionRetractionRetryService.getInstance().start();
  ImageUpdateService.getInstance().start();
  SchedulerService.getInstance().start();
  MfaService.getInstance().start();
  MeshService.getInstance().start().catch((err) => {
    console.warn('[Startup] MeshService start failed:', (err as Error).message);
  });
  BlueprintReconciler.getInstance().start();
  CveIntelService.getInstance().start();

  // Drop the cached /api/meta entry on tunnel reconnect so the next
  // /api/nodes/:id/meta refetches fresh capabilities and version through
  // the live loopback bridge instead of waiting for the 3-minute TTL.
  // Also flush durable mute-replica retractions that waited for this node.
  PilotTunnelManager.getInstance().on('tunnel-up', (nodeId: number) => {
    invalidateRemoteMetaCache(nodeId);
    void SuppressionRetractionRetryService.getInstance().flushNode(nodeId);
    void RegistryDeliveryReconciler.getInstance().reconcileNode(nodeId);
  });

  // Most async initializers still run in parallel. Docker event monitoring
  // is sequenced after self identity so it never classifies Sencho's own
  // container as a routeable stack event.
  await Promise.all([
    SelfUpdateService.getInstance().initialize(),
    (async () => {
      await SelfIdentityService.getInstance().initialize();
      clearSelfContainerNotificationRouting();
      await DockerEventManager.getInstance().start();
    })(),
    TrivyService.getInstance().initialize(),
  ]);
  void ImageOperationService.getInstance().reconcileOnStartup().catch((error) => {
    console.error('[ImageOperation] Startup reconciliation failed:', error);
  });

  // Fire-and-forget housekeeping; logged but never awaited.
  sweepStaleGitTempDirs().catch((err) => {
    console.warn('[GitSource] Temp dir sweep failed:', (err as Error).message);
  });
  sweepStaleTrivyTempDirs().catch((err) => {
    console.warn('[Trivy] Temp dir sweep failed:', (err as Error).message);
  });

  const isPilotAgent = isPilotMode();
  const listenHost = isPilotAgent ? '127.0.0.1' : undefined;

  server.listen(PORT, listenHost, () => {
    console.log(`Server running on ${listenHost || '0.0.0.0'}:${PORT}${isPilotAgent ? ' (pilot-agent mode)' : ''}`);
    if (isPilotAgent) {
      import('../pilot/agent').then((m) => m.startPilotAgent(PORT)).catch((err) => {
        console.error('[Pilot] Agent startup failed:', err);
      });
    }
  });
}
