import { randomBytes } from "node:crypto";
import { lstat, open, rename, unlink, type FileHandle } from "node:fs/promises";
import { basename, dirname, isAbsolute, join } from "node:path";
import type { Prisma, PrismaClient } from "@prisma/client";
import { rewrapStoredProviderEnvelopes } from "../connections/providerConnections.js";
import { rewrapStoredFeishuApplicationEnvelopes } from "../connections/feishuApplicationConnections.js";
import {
  LocalSecretEnvelope,
  loadLocalSecretKeyringFile,
  type LocalSecretKeyring,
} from "./secretEnvelope.js";

export interface RotateLocalKekInput {
  readonly keyringFile: string;
  readonly prisma: PrismaClient;
  readonly now?: Date;
  readonly generatedKey?: Buffer;
  readonly expectedOwner?: { readonly uid: number; readonly gid: number };
}

export interface RotateLocalKekResult {
  readonly previousActiveKeyId: string;
  readonly activeKeyId: string;
  readonly verified: number;
  readonly rewrapped: number;
}

export const LOCAL_KEK_ROTATION_ADVISORY_LOCK = 7_310_579_862_737_525_041n;

/**
 * Atomically installs a new active KEK while retaining all previous keys, then
 * rewraps database DEKs. A crash after the file rename is recoverable because
 * both old and new KEKs remain in the keyring and the operation is rerunnable.
 */
export async function rotateLocalKek(input: RotateLocalKekInput): Promise<RotateLocalKekResult> {
  if (!isAbsolute(input.keyringFile)) {
    throw new Error("local secret keyring path must be absolute");
  }
  const expectedOwner = input.expectedOwner ?? { uid: 0, gid: 0 };
  const initialMetadata = await lstat(input.keyringFile);
  assertKeyringMetadata(initialMetadata, expectedOwner);
  return input.prisma.$transaction(async (tx) => {
    const rows = await tx.$queryRaw<Array<{ readonly locked: boolean }>>`
      SELECT pg_try_advisory_xact_lock(${LOCAL_KEK_ROTATION_ADVISORY_LOCK}) AS "locked"
    `;
    if (rows[0]?.locked !== true) {
      throw new Error("local KEK rotation lock is already held");
    }
    return rotateUnderLock(input, expectedOwner, tx);
  }, { maxWait: 5_000, timeout: 300_000 });
}

async function rotateUnderLock(
  input: RotateLocalKekInput,
  expectedOwner: { readonly uid: number; readonly gid: number },
  tx: Prisma.TransactionClient,
): Promise<RotateLocalKekResult> {
  const metadata = await lstat(input.keyringFile);
  assertKeyringMetadata(metadata, expectedOwner);

  const previous = await loadLocalSecretKeyringFile(input.keyringFile);
  const generatedKey = input.generatedKey === undefined
    ? randomBytes(32)
    : Buffer.from(input.generatedKey);
  if (generatedKey.byteLength !== 32) {
    generatedKey.fill(0);
    throw new Error("generated local KEK must contain exactly 32 bytes");
  }
  const keyId = nextKeyId(input.now ?? new Date());
  if (previous.keys.has(keyId)) {
    generatedKey.fill(0);
    throw new Error(`generated local KEK identifier already exists: ${keyId}`);
  }

  const keys = new Map(previous.keys);
  keys.set(keyId, generatedKey);
  const keyring: LocalSecretKeyring = { activeKeyId: keyId, keys };
  try {
    await replaceKeyringAtomically(input.keyringFile, keyring, metadata.uid, metadata.gid);
    const providerResult = await rewrapStoredProviderEnvelopes(
      tx,
      new LocalSecretEnvelope(keyring),
    );
    const feishuResult = await rewrapStoredFeishuApplicationEnvelopes(
      tx,
      new LocalSecretEnvelope(keyring),
    );
    return {
      previousActiveKeyId: previous.activeKeyId,
      activeKeyId: keyId,
      verified: providerResult.verified + feishuResult.verified,
      rewrapped: providerResult.rewrapped + feishuResult.rewrapped,
    };
  } finally {
    generatedKey.fill(0);
  }
}

function assertKeyringMetadata(
  metadata: {
    readonly mode: number;
    readonly uid: number;
    readonly gid: number;
    isFile(): boolean;
    isSymbolicLink(): boolean;
  },
  expectedOwner: { readonly uid: number; readonly gid: number },
): void {
  if (metadata.isSymbolicLink() || !metadata.isFile()) {
    throw new Error("local secret keyring must be a regular non-symlink file");
  }
  if (metadata.uid !== expectedOwner.uid || metadata.gid !== expectedOwner.gid) {
    throw new Error(
      `local secret keyring must be owned by ${expectedOwner.uid}:${expectedOwner.gid}`,
    );
  }
  if ((metadata.mode & 0o777) !== 0o600) {
    throw new Error("local secret keyring must have mode 0600");
  }
}

async function replaceKeyringAtomically(
  file: string,
  keyring: LocalSecretKeyring,
  uid: number,
  gid: number,
): Promise<void> {
  const directory = dirname(file);
  const temporary = join(directory, `.${basename(file)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
  const encodedKeys = Object.fromEntries(
    [...keyring.keys].map(([keyId, key]) => [keyId, key.toString("base64")]),
  );
  const contents = `${JSON.stringify({ version: 1, activeKeyId: keyring.activeKeyId, keys: encodedKeys })}\n`;
  let handle: FileHandle | undefined;
  try {
    handle = await open(temporary, "wx", 0o600);
    await handle.chown(uid, gid);
    await handle.writeFile(contents, "utf8");
    await handle.sync();
    await handle.close();
    handle = undefined;
    await rename(temporary, file);
    await syncDirectory(directory);
  } catch (error) {
    const cleanupFailures: unknown[] = [];
    if (handle !== undefined) {
      try {
        await handle.close();
      } catch (cleanupError) {
        cleanupFailures.push(cleanupError);
      }
    }
    try {
      await removeIfExists(temporary);
    } catch (cleanupError) {
      cleanupFailures.push(cleanupError);
    }
    if (cleanupFailures.length > 0) {
      throw new AggregateError(
        [error, ...cleanupFailures],
        "local keyring replacement and cleanup both failed",
        { cause: error },
      );
    }
    throw error;
  }
}

async function syncDirectory(directory: string): Promise<void> {
  const handle = await open(directory, "r");
  let syncFailure: unknown;
  try {
    await handle.sync();
  } catch (error) {
    syncFailure = error;
  }
  let closeFailure: unknown;
  try {
    await handle.close();
  } catch (error) {
    closeFailure = error;
  }
  if (syncFailure !== undefined && closeFailure !== undefined) {
    throw new AggregateError(
      [syncFailure, closeFailure],
      "keyring directory sync and handle close both failed",
      { cause: syncFailure },
    );
  }
  if (syncFailure !== undefined) throw syncFailure;
  if (closeFailure !== undefined) throw closeFailure;
}

async function removeIfExists(file: string): Promise<void> {
  try {
    await unlink(file);
  } catch (error) {
    if (isErrno(error, "ENOENT")) return;
    throw error;
  }
}

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

function nextKeyId(now: Date): string {
  const timestamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
  return `local-${timestamp}-${randomBytes(4).toString("hex")}`;
}
