#!/usr/bin/env node
import { execFile } from "node:child_process";
import { constants } from "node:fs";
import { access, lstat, readFile, realpath, stat } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { PrismaClient } from "@prisma/client";
import { parse } from "dotenv";
import { LocalSecretEnvelope, loadLocalSecretKeyring } from "../security/secretEnvelope.js";
import { verifyStoredProviderEnvelopes } from "../connections/providerConnections.js";
import { verifyStoredFeishuApplicationEnvelopes } from "../connections/feishuApplicationConnections.js";
import {
  DeploymentPreflightError,
  validateDeploymentPreflight,
  type DeploymentEnv,
  type DeploymentPreflightInput,
  type DeploymentPreflightResult,
} from "./preflight.js";

const execFileAsync = promisify(execFile);

interface Options {
  readonly envFile: string;
  readonly keyringFile: string;
  readonly nodeBin: string;
  readonly runuserBin: string;
  readonly setprivBin: string;
  readonly serviceUser: string;
  readonly baseDir: string;
  readonly hubDir: string;
  readonly serviceHome: string;
  readonly stateDir: string;
  readonly cacheDir: string;
  readonly workspaceRoot: string;
  readonly serviceUnit: string;
  readonly bwrapBin: string;
  readonly socatBin: string;
  readonly pgIsReadyBin: string;
  readonly serviceUid?: number;
  readonly serviceGid?: number;
}

async function main(): Promise<void> {
  const options = parseArgs(process.argv.slice(2));
  const envFileContents = await readFile(options.envFile);
  const env = parse(envFileContents) satisfies DeploymentEnv;
  const rawInput = deploymentInput(options, env);

  // Validate lexical paths and values before touching any external dependency.
  validateDeploymentPreflight(rawInput);

  const canonicalInput = await canonicalizeInput(rawInput);
  const result = validateDeploymentPreflight(canonicalInput);
  await validateSecretFile(options.envFile);
  const secretEnvelope = await validateKeyringFile(options.keyringFile);
  await validateNodeVersion(options.nodeBin);
  await validateExecutable(options.runuserBin, "runuser");
  await validateExecutable(options.setprivBin, "setpriv");
  await validateExecutable(options.bwrapBin, "bubblewrap");
  await validateExecutable(options.socatBin, "socat");
  await validateExecutable(result.cphBin, "cph");
  await validateExecutable(options.pgIsReadyBin, "pg_isready");
  await validateCph(result.cphBin);
  await validatePostgres(options.pgIsReadyBin, result);
  await validatePostgresQuery(result.databaseUrl, secretEnvelope);

  if (options.serviceUid !== undefined && options.serviceGid !== undefined) {
    await validateProvisionedDirectories(canonicalInput, options.serviceUid, options.serviceGid);
    await validateServiceRuntime(options, rawInput, canonicalInput, result);
  }

  console.log("[preflight] configuration and prerequisites valid");
}

function deploymentInput(options: Options, env: DeploymentEnv): DeploymentPreflightInput {
  return {
    baseDir: options.baseDir,
    hubDir: options.hubDir,
    serviceHome: options.serviceHome,
    stateDir: options.stateDir,
    cacheDir: options.cacheDir,
    workspaceRoot: options.workspaceRoot,
    expectedServiceUnit: options.serviceUnit,
    env,
  };
}

async function canonicalizeInput(input: DeploymentPreflightInput): Promise<DeploymentPreflightInput> {
  const baseDir = await canonicalizeProspectivePath(input.baseDir);
  const hubDir = await canonicalizeProspectivePath(input.hubDir);
  const serviceHome = await canonicalizeProspectivePath(input.serviceHome);
  const stateDir = await canonicalizeProspectivePath(input.stateDir);
  const cacheDir = await canonicalizeProspectivePath(input.cacheDir);
  const workspaceRoot = await canonicalizeProspectivePath(input.workspaceRoot);
  return {
    baseDir,
    hubDir,
    serviceHome,
    stateDir,
    cacheDir,
    workspaceRoot,
    expectedServiceUnit: input.expectedServiceUnit,
    env: { ...input.env, HUB_PROJECT_WORKSPACE_ROOT: workspaceRoot },
  };
}

async function canonicalizeProspectivePath(value: string): Promise<string> {
  const absolute = resolve(value);
  const missingComponents: string[] = [];
  let candidate = absolute;

  while (true) {
    try {
      const existing = await realpath(candidate);
      return resolve(existing, ...missingComponents.reverse());
    } catch (error) {
      if (!isNodeError(error) || error.code !== "ENOENT") throw error;
      const parent = dirname(candidate);
      if (parent === candidate) throw error;
      missingComponents.push(basename(candidate));
      candidate = parent;
    }
  }
}

async function validateSecretFile(path: string): Promise<void> {
  const metadata = await stat(path);
  if (!metadata.isFile()) {
    throw new Error(`environment file is not a regular file: ${path}`);
  }
  if ((metadata.mode & 0o077) !== 0) {
    throw new Error(`environment file must not be accessible by group or others: ${path}`);
  }
}

async function validateKeyringFile(path: string): Promise<LocalSecretEnvelope> {
  const metadata = await lstat(path);
  if (metadata.isSymbolicLink() || !metadata.isFile()) {
    throw new Error(`local secret keyring is not a regular non-symlink file: ${path}`);
  }
  if ((metadata.mode & 0o077) !== 0) {
    throw new Error(`local secret keyring must not be accessible by group or others: ${path}`);
  }
  const keyring = await loadLocalSecretKeyring({
    NODE_ENV: "preflight",
    HUB_SECRET_KEYRING_FILE: path,
  });
  console.log("[preflight] local secret keyring is valid");
  return new LocalSecretEnvelope(keyring);
}

async function validateNodeVersion(nodeBin: string): Promise<void> {
  let stdout: string;
  try {
    ({ stdout } = await execFileAsync(nodeBin, ["--version"], { timeout: 10_000 }));
  } catch (error) {
    throw new Error(`Node.js version check failed: ${nodeBin} (${formatFailure(error)})`, { cause: error });
  }
  const version = stdout.trim().replace(/^v/, "");
  const major = Number(version.split(".")[0]);
  if (!Number.isInteger(major) || major < 24) {
    throw new Error(`Node.js 24 or newer is required; found ${version || "unknown"}`);
  }
  console.log(`[preflight] Node.js: ${version}`);
}

async function validateExecutable(path: string, label: string): Promise<void> {
  try {
    await access(path, constants.X_OK);
  } catch (error) {
    throw new Error(
      `${label} executable is missing or not executable: ${path} (${formatFailure(error)})`,
      { cause: error },
    );
  }
}

async function validateCph(cphBin: string): Promise<void> {
  let stdout: string;
  try {
    ({ stdout } = await execFileAsync(cphBin, ["--version"], { timeout: 10_000 }));
  } catch (error) {
    throw new Error(`cph --version failed: ${cphBin} (${formatFailure(error)})`, { cause: error });
  }
  if (!/^cph \d+\.\d+\.\d+(?:\s|$)/m.test(stdout)) {
    throw new Error(`cph --version returned an unexpected value: ${cphBin}`);
  }
  console.log(`[preflight] cph: ${stdout.trim()}`);
}

async function validatePostgres(pgIsReadyBin: string, result: DeploymentPreflightResult): Promise<void> {
  const url = new URL(result.databaseUrl);
  const postgresEnv: NodeJS.ProcessEnv = {
    ...process.env,
    PGCONNECT_TIMEOUT: "5",
    PGHOST: url.hostname,
    PGPORT: url.port || "5432",
    PGUSER: decodeURIComponent(url.username),
    PGPASSWORD: decodeURIComponent(url.password),
    PGDATABASE: decodeURIComponent(url.pathname.replace(/^\//, "")),
  };
  const sslMode = url.searchParams.get("sslmode");
  if (sslMode !== null) postgresEnv["PGSSLMODE"] = sslMode;

  try {
    await execFileAsync(pgIsReadyBin, ["--quiet"], { env: postgresEnv, timeout: 10_000 });
  } catch (error) {
    throw new Error(
      `PostgreSQL is not accepting connections for DATABASE_URL (${formatFailure(error)})`,
      { cause: error },
    );
  }
  console.log("[preflight] PostgreSQL is accepting connections");
}

async function validatePostgresQuery(
  databaseUrl: string,
  secretEnvelope: LocalSecretEnvelope,
): Promise<void> {
  const client = new PrismaClient({
    datasources: { db: { url: databaseUrl } },
    log: [],
  });
  let queryFailure: unknown;
  const envelopeFailures: Array<{ readonly kind: "provider" | "Feishu"; readonly error: unknown }> = [];
  try {
    await client.$queryRawUnsafe("SELECT 1");
    const migrationTable = await client.$queryRaw<Array<{ relation: string | null }>>`
      SELECT to_regclass('public."_prisma_migrations"')::text AS "relation"
    `;
    if (migrationTable[0]?.relation === null || migrationTable[0]?.relation === undefined) {
      console.log("[preflight] empty database has no migration table or stored connection envelopes");
    } else {
      const providerApplied = await migrationApplied(
        client,
        "20260710220000_org_provider_secret_envelopes",
      );
      if (!providerApplied) {
        console.log("[preflight] provider envelope migration is pending; no stored envelopes to authenticate");
      } else {
        try {
          const verified = await verifyStoredProviderEnvelopes(client, secretEnvelope);
          console.log(`[preflight] authenticated ${verified} stored provider envelope(s)`);
        } catch (error) {
          envelopeFailures.push({ kind: "provider", error });
        }
      }
      const feishuApplied = await migrationApplied(
        client,
        "20260710230000_org_feishu_application_secrets",
      );
      if (!feishuApplied) {
        console.log("[preflight] Feishu envelope migration is pending; no stored envelopes to authenticate");
      } else {
        try {
          const verified = await verifyStoredFeishuApplicationEnvelopes(client, secretEnvelope);
          console.log(`[preflight] authenticated ${verified} stored Feishu envelope(s)`);
        } catch (error) {
          envelopeFailures.push({ kind: "Feishu", error });
        }
      }
    }
  } catch (error) {
    queryFailure = error;
  }

  let disconnectFailure: unknown;
  try {
    await client.$disconnect();
  } catch (error) {
    disconnectFailure = error;
  }

  if (queryFailure !== undefined) {
    const disconnectDetail =
      disconnectFailure === undefined ? "" : `; disconnect also failed (${formatFailure(disconnectFailure)})`;
    throw new Error(
      `PostgreSQL authenticated query failed for DATABASE_URL (${formatFailure(queryFailure)})${disconnectDetail}`,
      { cause: queryFailure },
    );
  }
  if (disconnectFailure !== undefined) {
    throw new Error(
      `PostgreSQL preflight disconnect failed (${formatFailure(disconnectFailure)})`,
      { cause: disconnectFailure },
    );
  }
  if (envelopeFailures.length > 0) {
    const failures = envelopeFailures.map(({ error }) => error);
    const cause = failures.length === 1
      ? failures[0]
      : new AggregateError(failures, "multiple connection envelope verifications failed");
    const details = envelopeFailures
      .map(({ kind, error }) => `${kind}: ${formatFailure(error)}`)
      .join("; ");
    throw new Error(
      `stored connection envelope verification failed (${details})`,
      { cause },
    );
  }
  console.log("[preflight] PostgreSQL authenticated query succeeded");
}

async function migrationApplied(client: PrismaClient, migrationName: string): Promise<boolean> {
  const rows = await client.$queryRaw<Array<{ finished_at: Date | null; rolled_back_at: Date | null }>>`
    SELECT "finished_at", "rolled_back_at"
    FROM "_prisma_migrations"
    WHERE "migration_name" = ${migrationName}
    ORDER BY "started_at" DESC
    LIMIT 1
  `;
  return rows[0]?.finished_at !== null && rows[0]?.finished_at !== undefined &&
    rows[0]?.rolled_back_at === null;
}

async function validateProvisionedDirectories(
  input: DeploymentPreflightInput,
  serviceUid: number,
  serviceGid: number,
): Promise<void> {
  const directories = [input.serviceHome, input.stateDir, input.cacheDir, input.workspaceRoot];
  for (const directory of directories) {
    const metadata = await stat(directory);
    if (!metadata.isDirectory()) {
      throw new Error(`provisioned path is not a directory: ${directory}`);
    }
    if (metadata.uid !== serviceUid || metadata.gid !== serviceGid) {
      throw new Error(`provisioned directory has unexpected ownership: ${directory}`);
    }
    if ((metadata.mode & 0o777) !== 0o750) {
      throw new Error(`provisioned directory must have mode 0750: ${directory}`);
    }
  }
  console.log(`[preflight] service directories owned by ${serviceUid}:${serviceGid} with mode 0750`);
}

async function validateServiceRuntime(
  options: Options,
  configuredInput: DeploymentPreflightInput,
  canonicalInput: DeploymentPreflightInput,
  result: DeploymentPreflightResult,
): Promise<void> {
  const serviceEnv: NodeJS.ProcessEnv = {
    ...configuredInput.env,
    NODE_ENV: "production",
    DATABASE_URL: result.databaseUrl,
    HOME: configuredInput.serviceHome,
    XDG_STATE_HOME: configuredInput.stateDir,
    XDG_CACHE_HOME: configuredInput.cacheDir,
    PATH: [dirname(options.nodeBin), dirname(options.bwrapBin), dirname(options.socatBin), dirname(result.cphBin), "/usr/bin", "/bin"].join(":"),
  };
  const pathAccessRequirements = (input: DeploymentPreflightInput) => [
    [input.hubDir, constants.R_OK | constants.X_OK],
    [join(input.hubDir, "dist"), constants.R_OK | constants.X_OK],
    [join(input.hubDir, "node_modules"), constants.R_OK | constants.X_OK],
    [join(input.hubDir, "prisma"), constants.R_OK | constants.X_OK],
    [input.serviceHome, constants.R_OK | constants.W_OK | constants.X_OK],
    [input.stateDir, constants.R_OK | constants.W_OK | constants.X_OK],
    [input.cacheDir, constants.R_OK | constants.W_OK | constants.X_OK],
    [input.workspaceRoot, constants.R_OK | constants.W_OK | constants.X_OK],
    [join(input.hubDir, "dist", "server.js"), constants.R_OK],
    [join(input.hubDir, "dist", "deployment", "service-runtime-probe.js"), constants.R_OK],
    [join(input.hubDir, "node_modules", "prisma", "build", "index.js"), constants.R_OK],
    [join(input.hubDir, "prisma", "schema.prisma"), constants.R_OK],
  ] as const;
  const accessRequirements = [
    ...pathAccessRequirements(configuredInput),
    ...pathAccessRequirements(canonicalInput),
    [options.nodeBin, constants.X_OK],
    [options.bwrapBin, constants.X_OK],
    [options.socatBin, constants.X_OK],
    [options.setprivBin, constants.X_OK],
    [result.cphBin, constants.X_OK],
  ] as const;
  const accessProbe = [
    'const { accessSync } = require("node:fs");',
    "for (const [path, mode] of JSON.parse(process.argv[1])) accessSync(path, mode);",
  ].join("\n");

  await runAsService(
    options,
    serviceEnv,
    options.nodeBin,
    ["-e", accessProbe, JSON.stringify(accessRequirements)],
    "release and persistent path access probe",
  );
  await runAsService(
    options,
    serviceEnv,
    options.nodeBin,
    ["--check", join(configuredInput.hubDir, "dist", "server.js")],
    "built Hub syntax probe",
  );
  await runAsService(
    options,
    serviceEnv,
    options.nodeBin,
    [join(configuredInput.hubDir, "node_modules", "prisma", "build", "index.js"), "--version"],
    "Prisma runtime probe",
  );
  await runAsService(
    options,
    serviceEnv,
    options.nodeBin,
    [join(configuredInput.hubDir, "dist", "deployment", "service-runtime-probe.js")],
    "Hub dependency and authenticated database probe",
  );
  await runAsService(options, serviceEnv, result.cphBin, ["--version"], "cph runtime probe");
  await runAsService(options, serviceEnv, options.socatBin, ["-V"], "socat runtime probe");
  await runAsService(
    options,
    serviceEnv,
    options.bwrapBin,
    [
      "--unshare-all",
      "--die-with-parent",
      "--new-session",
      "--ro-bind",
      "/",
      "/",
      "--proc",
      "/proc",
      "--dev",
      "/dev",
      "/bin/true",
    ],
    "bubblewrap user-namespace probe",
  );
  console.log(`[preflight] runtime prerequisites execute as service user ${options.serviceUser}`);
}

async function runAsService(
  options: Options,
  env: NodeJS.ProcessEnv,
  command: string,
  args: readonly string[],
  label: string,
): Promise<void> {
  try {
    await execFileAsync(
      options.runuserBin,
      [
        "-u",
        options.serviceUser,
        "--",
        options.setprivBin,
        "--no-new-privs",
        command,
        ...args,
      ],
      { cwd: options.hubDir, env, timeout: 15_000, maxBuffer: 2 * 1024 * 1024 },
    );
  } catch (error) {
    throw new Error(`${label} failed for service user ${options.serviceUser} (${formatFailure(error)})`, {
      cause: error,
    });
  }
}

function parseArgs(argv: readonly string[]): Options {
  const values = new Map<string, string>();
  for (let index = 0; index < argv.length; index += 2) {
    const key = argv[index];
    const value = argv[index + 1];
    if (key === undefined || value === undefined || !key.startsWith("--")) {
      throw new Error("preflight arguments must be --name value pairs");
    }
    if (values.has(key)) throw new Error(`duplicate preflight argument: ${key}`);
    values.set(key, value);
  }

  const allowed = new Set([
    "--env-file",
    "--keyring-file",
    "--node-bin",
    "--runuser-bin",
    "--setpriv-bin",
    "--service-user",
    "--base-dir",
    "--hub-dir",
    "--service-home",
    "--state-dir",
    "--cache-dir",
    "--workspace-root",
    "--service-unit",
    "--bwrap-bin",
    "--socat-bin",
    "--pg-isready-bin",
    "--service-uid",
    "--service-gid",
  ]);
  for (const key of values.keys()) {
    if (!allowed.has(key)) throw new Error(`unknown preflight argument: ${key}`);
  }

  const serviceUid = optionalId(values, "--service-uid");
  const serviceGid = optionalId(values, "--service-gid");
  if ((serviceUid === undefined) !== (serviceGid === undefined)) {
    throw new Error("--service-uid and --service-gid must be supplied together");
  }

  return {
    envFile: required(values, "--env-file"),
    keyringFile: required(values, "--keyring-file"),
    nodeBin: required(values, "--node-bin"),
    runuserBin: required(values, "--runuser-bin"),
    setprivBin: required(values, "--setpriv-bin"),
    serviceUser: required(values, "--service-user"),
    baseDir: required(values, "--base-dir"),
    hubDir: required(values, "--hub-dir"),
    serviceHome: required(values, "--service-home"),
    stateDir: required(values, "--state-dir"),
    cacheDir: required(values, "--cache-dir"),
    workspaceRoot: required(values, "--workspace-root"),
    serviceUnit: required(values, "--service-unit"),
    bwrapBin: required(values, "--bwrap-bin"),
    socatBin: required(values, "--socat-bin"),
    pgIsReadyBin: required(values, "--pg-isready-bin"),
    ...(serviceUid === undefined ? {} : { serviceUid, serviceGid: serviceGid! }),
  };
}

function required(values: ReadonlyMap<string, string>, key: string): string {
  const value = values.get(key);
  if (value === undefined || value === "") throw new Error(`missing required preflight argument: ${key}`);
  return value;
}

function optionalId(values: ReadonlyMap<string, string>, key: string): number | undefined {
  const raw = values.get(key);
  if (raw === undefined) return undefined;
  const value = Number(raw);
  if (!Number.isInteger(value) || value < 0) throw new Error(`${key} must be a non-negative integer`);
  return value;
}

function isNodeError(error: unknown): error is NodeJS.ErrnoException {
  return error instanceof Error && "code" in error;
}

function formatFailure(error: unknown): string {
  if (!(error instanceof Error)) return String(error);

  const details: string[] = [];
  const processError = error as NodeJS.ErrnoException & {
    readonly signal?: NodeJS.Signals | null;
    readonly stderr?: string | Buffer;
  };
  if (processError.code !== undefined) details.push(`exit=${String(processError.code)}`);
  if (processError.signal !== undefined && processError.signal !== null) {
    details.push(`signal=${processError.signal}`);
  }
  const stderr = processError.stderr?.toString().trim();
  if (stderr !== undefined && stderr !== "") details.push(`stderr=${boundedDiagnostic(redactDiagnostic(stderr))}`);
  if (details.length === 0) details.push(boundedDiagnostic(redactDiagnostic(error.message)));
  return details.join(", ");
}

function boundedDiagnostic(value: string): string {
  const limit = 2_000;
  if (value.length <= limit) return value;
  return `${value.slice(0, limit)}…[truncated ${value.length - limit} chars]`;
}

function redactDiagnostic(value: string): string {
  return value
    .replace(/\b(postgres(?:ql)?):\/\/([^:\s/@]+):([^@\s]+)@/gi, "$1://$2:[REDACTED]@")
    .replace(/\b(password\s*[:=]\s*)\S+/gi, "$1[REDACTED]");
}

main().catch((error: unknown) => {
  if (error instanceof DeploymentPreflightError) {
    console.error(`[preflight] ${error.message}`);
  } else {
    console.error(`[preflight] failed: ${error instanceof Error ? error.message : String(error)}`);
  }
  process.exitCode = 1;
});
