// FU-010 Path A (Unit 13, 2026-05-04): secret-boot-guard MUST be imported
// before any other module in this file. Its module evaluation now runs the
// guard via top-level self-execution, which throws synchronously if any
// required signing/encryption secret is missing — closing the theoretical
// window where modules imported by `./routes` could capture hardcoded
// dev-fallback literals into top-level const bindings before the explicit
// guard call ran. The explicit `enforceSecretBootGuard()` invocation below
// is preserved as a belt-and-braces semantic checkpoint right before HTTP
// bind; the second call is an idempotent re-audit. See secret-boot-guard.ts
// tail comment for the full rationale and the Unit-7 audit findings.
import { enforceSecretBootGuard } from "./lib/secret-boot-guard";
import { assertFaultInjectionDeploymentSafety } from "./lib/fault-injection";
import express, { type Request, Response, NextFunction } from "express";
import { registerRoutes } from "./routes";
import { redactedError } from "./lib/error-handler";
import { serveStatic } from "./static";
import { createServer } from "http";

// Belt-and-braces explicit invocation — pre-HTTP-bind semantic checkpoint.
// The substantive guard already ran during the import on the previous line
// (FU-010 Path A). Running again here is idempotent and harmless; it
// preserves the explicit "this is the gate before HTTP bind" semantics
// even if the import order is later refactored.
enforceSecretBootGuard();

const app = express();
const httpServer = createServer(app);

declare module "http" {
  interface IncomingMessage {
    rawBody: unknown;
  }
}

app.use(
  express.json({
    verify: (req, _res, buf) => {
      req.rawBody = buf;
    },
  }),
);

app.use(express.urlencoded({ extended: false }));

// Convert body-parser strict-mode JSON SyntaxErrors into 400s instead of letting
// them bubble up to the global error handler as 500s. Caught by Wave-10 fuzz harness.
app.use((err: any, _req: Request, res: Response, next: NextFunction) => {
  if (err && err.type === "entity.parse.failed") {
    return res.status(400).json({ error: "INVALID_JSON", message: "Request body is not valid JSON." });
  }
  next(err);
});

export function log(message: string, source = "express") {
  const formattedTime = new Date().toLocaleTimeString("en-US", {
    hour: "numeric",
    minute: "2-digit",
    second: "2-digit",
    hour12: true,
  });

  // CO-002: intentional raw console.log — top-level `log()` helper is itself
  // the bootstrap-tier sink used by AegisLogger and by code paths that run
  // before logger.ts is safely importable. Routing this through the logger
  // would be circular.
  // eslint-disable-next-line no-console
  console.log(`${formattedTime} [${source}] ${message}`);
}

app.use((req, res, next) => {
  const start = Date.now();
  const path = req.path;

  res.on("finish", () => {
    const duration = Date.now() - start;
    if (path.startsWith("/api")) {
      // Request log: method / path / status / duration ONLY. The response BODY is DELIBERATELY NOT
      // logged. On a bank platform the /api response bodies contain PII — and on the PII routes
      // (KYC, customers) kycCustomerToDto returns DECRYPTED names / national IDs / phones / DOB — so
      // writing them to stdout on a 200 undoes the AES-256-GCM at-rest-encryption posture the moment
      // logs are aggregated / retained / shipped to an observability vendor (verified 2026-07-09: the
      // OFAC screening endpoints were logging the customer name in full on the success path). The
      // Replit-scaffold logger captured `res.json` bodies (originally with an 80-char truncation that
      // had since been removed) — that res.json monkey-patch + body append are removed here. DO NOT
      // reintroduce response-body logging (truncated or not); truncation still leaks the leading
      // identifying fields and keeps bodies in logs at all.
      log(`${req.method} ${path} ${res.statusCode} in ${duration}ms`);
    }
  });

  next();
});

(async () => {
  // VF / Rule 14-style fail-fast: refuse to boot if the deliberate
  // fault-injection primitive is armed inside a real deployment — a Replit
  // deployment (REPLIT_DEPLOYMENT=1) OR a production runtime (NODE_ENV=production,
  // e.g. the bank-laptop Docker deployment; Rule 7 dual-environment coverage).
  // No-op in dev/test (NODE_ENV=development/unset, REPLIT_DEPLOYMENT unset) so the
  // verification path that legitimately arms FAULT_INJECTION_ENABLED is
  // unaffected. Runs before registerRoutes()/serving — a dangerous flag
  // combination must crash the boot, never serve with the deliberate-fault paths
  // live.
  assertFaultInjectionDeploymentSafety();

  // T003 — RLS policy initialization (architect Rule 9 PASS 2026-06-03 §0.2).
  // Must complete before any route registers — a partial or failed RLS
  // deployment must hard-abort the boot, not leave routes serving unprotected.
  //
  // Q5 binding: explicit try/catch + process.exit(1).
  // process.on("unhandledRejection") is registered at line ~225 (AFTER
  // registerRoutes), so it CANNOT catch a rejection from here. Explicit
  // try/catch is required — relying on the unhandledRejection handler would
  // leave a window where the server starts accepting requests on a partially
  // initialised RLS state.
  //
  // [boot-timing] (2026-06-16): per-phase durationMs instrumentation so prod
  // cold-boot improvements/regressions are directly measurable from the logs.
  // Rule 15: these lines report ELAPSED TIME ONLY — they assert nothing about
  // security-control correctness (each phase keeps its own fail-fast assertion).
  const bootStartedAt = Date.now();
  const phase = async <T>(name: string, fn: () => Promise<T>): Promise<T> => {
    const t0 = Date.now();
    const out = await fn();
    log(`${name} completed in ${Date.now() - t0}ms`, "boot-timing");
    return out;
  };
  try {
    const { applyBootSchemaMigrations, initRlsPolicies, cleanupT003ProbeArtifacts } = await import("./lib/rls-init");
    const { assertBootSchemaConformance } = await import("./lib/schema-conformance");
    // FU-054 Deliverable A (2026-06-04): apply missing-column migrations before
    // RLS policies. Schema must be structurally complete before any route serves.
    // Platform note: Publish deploys code only — no drizzle-kit push against prod.
    // Columns declared in Drizzle schema require an explicit startup migration here.
    await phase("applyBootSchemaMigrations", () => applyBootSchemaMigrations());
    // FU-054 Deliverable C (architect Rule 9 PASS 2026-06-04, D-A through D-F):
    // Schema-conformance boot assertion — verifies 113 security-critical columns
    // (69 TENANT_TABLES tenant_id, 43 PII, 1 migration) are present in the
    // PostgreSQL schema with the expected udt_name, before RLS policies are applied.
    // Catches column-absent and base-type mismatch (text vs varchar). Constraint/
    // nullability/default drift is explicitly out of scope (architect D-E ruling).
    // process.exit(1) via catch below on any failure (Rule 14).
    await phase("assertBootSchemaConformance", () => assertBootSchemaConformance());
    await phase("initRlsPolicies", () => initRlsPolicies());
    // T003 fix design brief v1.4 §3.3 (architect Rule 9 PASS 2026-06-04):
    // Probe-artifact cleanup runs after initRlsPolicies(), before registerRoutes().
    // Removes residual dsar_requests rows from the 2026-06-04 Option A diagnostic
    // run. Idempotent — no-op once rows are gone. Runs via ddlPool (superuser).
    await phase("cleanupT003ProbeArtifacts", () => cleanupT003ProbeArtifacts());
  } catch (err) {
    // CO-002: intentional raw console.error — pre-route fatal; structured
    // logger may not be initialised at this point in the boot sequence.
    // eslint-disable-next-line no-console
    console.error(
      "FATAL: RLS policy initialization failed — server cannot start safely:",
      err,
    );
    process.exit(1);
  }

  await phase("registerRoutes", () => registerRoutes(httpServer, app));

  // Tenant-binding seed + fail-closed outcome gate. Deliberately its OWN awaited
  // phase, AFTER registerRoutes and BEFORE httpServer.listen() below — so a
  // failure aborts the boot cleanly, before the port ever opens.
  //
  // These two were previously fired from registerRoutes as
  // `void seedSovereignTenant().then(() => seedUserTenantRoles())` — detached
  // from this await chain. A throw from there does NOT reach the try/catch that
  // wraps boot; it becomes an unhandled rejection at an arbitrary later moment,
  // racing the process.on("unhandledRejection") registration further down this
  // file (see the note at :111-113 — that handler is registered AFTER
  // registerRoutes and cannot catch a rejection raised from inside it). A
  // fail-closed gate whose caller discards the signal is not fail-closed.
  //
  // Nothing depended on the old detached timing: `void` means no caller could
  // await completion, and routes.ts:788's aegis-sovereign work already ran
  // BEFORE the seed fired. Ordering between the two seeds is preserved —
  // seedUserTenantRoles requires seedSovereignTenant (pilot-readiness-extras.ts:974).
  await phase("seedTenantBindings", async () => {
    const { seedSovereignTenant, seedUserTenantRoles } = await import("./lib/pilot-readiness-extras");
    await seedSovereignTenant();
    await seedUserTenantRoles();
  });

  app.use((err: any, _req: Request, res: Response, next: NextFunction) => {
    const status = err.status || err.statusCode || 500;
    // PII-safe extraction (redactedError): a drizzle/pg query error embeds the statement + params
    // (incl. row values / PII) in BOTH .message and .stack — logging or returning the raw error
    // leaks them. This handler previously did BOTH: console.error(err) wrote the raw message+stack to
    // stdout, and it returned err.message to the CLIENT on a 500. Both fixed 2026-07-09.
    const safe = redactedError(err);

    // CO-002: intentional raw console.error — Express top-level error handler; must not depend on the
    // logger module being importable in the failure path. Logs the REDACTED payload (code + safe
    // message + call frames), never the raw error object.
    // eslint-disable-next-line no-console
    console.error("Internal Server Error:", { status, ...safe });

    if (res.headersSent) {
      return next(err);
    }

    // Never return server internals to the caller: a 5xx gets a generic message (the drizzle dump
    // must not reach the client); an explicit 4xx returns its (redacted) message.
    const clientMessage = status >= 500 ? "Internal Server Error" : safe.message;
    return res.status(status).json({ message: clientMessage });
  });

  // importantly only setup vite in development and after
  // setting up all the other routes so the catch-all route
  // doesn't interfere with the other routes
  if (process.env.NODE_ENV === "production") {
    await phase("serveStatic", async () => serveStatic(app));
  } else {
    const { setupVite } = await import("./vite");
    await phase("setupVite", () => setupVite(httpServer, app));
  }

  // ALWAYS serve the app on the port specified in the environment variable PORT
  // Other ports are firewalled. Default to 5000 if not specified.
  // this serves both the API and the client.
  // It is the only port that is not firewalled.
  const port = parseInt(process.env.PORT || "5000", 10);
  httpServer.listen(
    {
      port,
      host: "0.0.0.0",
      reusePort: true,
    },
    () => {
      log(`serving on port ${port}`);
      log(
        `post-import boot window (migrations→listen) ${Date.now() - bootStartedAt}ms`,
        "boot-timing",
      );
    },
  );

  let isShuttingDown = false;

  async function gracefulShutdown(signal: string) {
    if (isShuttingDown) return;
    isShuttingDown = true;
    log(`${signal} received — starting graceful shutdown`, "shutdown");

    const forceTimeout = setTimeout(() => {
      log("Shutdown timeout reached — forcing exit", "shutdown");
      process.exit(1);
    }, 10000);

    try {
      await new Promise<void>((resolve, reject) => {
        httpServer.close((err) => {
          if (err) reject(err);
          else resolve();
        });
      });
      log("HTTP server closed", "shutdown");

      // T202 (2026-05-02) — stop the pilot-pack snapshot scheduler before we
      // close the DB pool. The scheduler holds a setInterval + a boot-time
      // setTimeout that would otherwise keep firing tick() after the pool is
      // gone, producing harmless-but-noisy "pool ended" errors during shutdown.
      try {
        const { stopPilotPackScheduler } = await import("./lib/pilot-pack-scheduler");
        stopPilotPackScheduler();
        log("Pilot-pack scheduler stopped", "shutdown");
      } catch (e) {
        log(`Pilot-pack scheduler stop skipped: ${e}`, "shutdown");
      }

      // FU-011 silent-pass conv #1 (2026-05-05): symmetric stop for the DSAR
      // purge worker scheduler. Same rationale as pilot-pack stop above —
      // clear the setInterval + boot setTimeout before the DB pool closes.
      try {
        const { stopDsarPurgeWorker } = await import("./lib/dsar-purge-worker");
        stopDsarPurgeWorker();
        log("DSAR purge worker scheduler stopped", "shutdown");
      } catch (e) {
        log(`DSAR purge worker stop skipped: ${e}`, "shutdown");
      }

      // FU-026 (2026-05-12): symmetric stop for the backup scheduler. Same
      // rationale as the two stops above — clear setInterval + boot setTimeout
      // before the DB pool closes.
      try {
        const { stopBackupScheduler } = await import("./lib/backup-scheduler");
        stopBackupScheduler();
        log("Backup scheduler stopped", "shutdown");
      } catch (e) {
        log(`Backup scheduler stop skipped: ${e}`, "shutdown");
      }

      // Audit-chain integrity (2026-07-09): drain the chain-write queue BEFORE
      // closing the DB pools. HTTP server is closed and all schedulers are
      // stopped above, so no new chain writes can enqueue — this flushes any
      // queued/in-flight write so it commits before the process exits. Without
      // it, a still-in-flight key-management chain write could commit AFTER the
      // successor process snapshotted the (pre-write) tail, forking the chain
      // (root cause of the 3 forks found 2026-07-08).
      try {
        const { drainChainWriteQueue } = await import("./lib/audit-chain");
        await drainChainWriteQueue();
        log("Audit-chain write queue drained", "shutdown");
      } catch (e) {
        log(`Audit-chain drain skipped: ${e}`, "shutdown");
      }

      // T003 dual-pool shutdown: end appPool (pool alias) and ddlPool both.
      // appPool (request path) and ddlPool (auditDb / DDL) are separate pg.Pool
      // instances; both must be ended to release connections cleanly.
      const { pool, ddlPool } = await import("./db");
      if (pool && typeof pool.end === "function") {
        await pool.end();
        log("App pool (aegis_app) closed", "shutdown");
      }
      if (ddlPool && typeof ddlPool.end === "function") {
        await ddlPool.end();
        log("DDL pool (postgres superuser) closed", "shutdown");
      }

      clearTimeout(forceTimeout);
      log("All resources released — exiting cleanly", "shutdown");
      process.exit(0);
    } catch (err) {
      log(`Error during shutdown: ${err}`, "shutdown");
      clearTimeout(forceTimeout);
      process.exit(1);
    }
  }

  process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
  process.on("SIGINT", () => gracefulShutdown("SIGINT"));

  process.on("uncaughtException", (err) => {
    log(`Uncaught exception: ${err.message}`, "fatal");
    // CO-002: intentional raw console.error — uncaughtException handler;
    // process is about to terminate, must not risk a logger-init failure.
    // eslint-disable-next-line no-console
    console.error(err.stack);
    gracefulShutdown("uncaughtException");
  });

  process.on("unhandledRejection", (reason) => {
    log(`Unhandled rejection: ${reason}`, "fatal");
    gracefulShutdown("unhandledRejection");
  });
})();
