import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";

const KEY_BYTES = 32;
const NONCE_BYTES = 12;
const AUTH_TAG_BYTES = 16;
const PRODUCTION_CREDENTIAL_NAME = "cph-secret-keyring";

export interface SecretBinding {
  readonly purpose: string;
  readonly organizationId: string;
  readonly connectionId: string;
  readonly secretVersionId: string;
}

export interface EncryptedPart {
  readonly nonce: string;
  readonly ciphertext: string;
  readonly authTag: string;
}

export interface SecretEnvelopeV1 {
  readonly version: 1;
  readonly algorithm: "AES-256-GCM";
  readonly keyId: string;
  readonly wrappedDek: EncryptedPart;
  readonly payload: EncryptedPart;
}

export interface LocalSecretKeyring {
  readonly activeKeyId: string;
  readonly keys: ReadonlyMap<string, Buffer>;
}

export type SecretKeyringEnv = Readonly<Record<string, string | undefined>>;

/**
 * Loads the one local KEK source accepted by the current runtime. Production
 * deliberately has no file-path fallback: systemd must materialize the fixed
 * credential below CREDENTIALS_DIRECTORY.
 */
export async function loadLocalSecretKeyring(
  env: SecretKeyringEnv = process.env,
): Promise<LocalSecretKeyring> {
  const nodeEnv = env["NODE_ENV"]?.trim();
  const configuredFile = env["HUB_SECRET_KEYRING_FILE"]?.trim();
  let file: string;

  if (nodeEnv === "production") {
    if (configuredFile !== undefined && configuredFile !== "") {
      throw new Error("production secret keyring must use the systemd credential");
    }
    const credentialsDirectory = env["CREDENTIALS_DIRECTORY"]?.trim();
    if (credentialsDirectory === undefined || credentialsDirectory === "") {
      throw new Error("missing required secret keyring setting: CREDENTIALS_DIRECTORY");
    }
    file = path.join(credentialsDirectory, PRODUCTION_CREDENTIAL_NAME);
  } else {
    if (configuredFile === undefined || configuredFile === "") {
      throw new Error("missing required secret keyring setting: HUB_SECRET_KEYRING_FILE");
    }
    file = configuredFile;
  }

  return loadLocalSecretKeyringFile(file);
}

export async function loadLocalSecretKeyringFile(file: string): Promise<LocalSecretKeyring> {
  let source: string;
  try {
    source = await readFile(file, "utf8");
  } catch (error) {
    throw new Error("local secret keyring is unavailable", { cause: error });
  }

  return parseKeyring(source);
}

export class LocalSecretEnvelope {
  constructor(private readonly keyring: LocalSecretKeyring) {}

  get activeKeyId(): string {
    return this.keyring.activeKeyId;
  }

  encryptJson(binding: SecretBinding, value: unknown): SecretEnvelopeV1 {
    validateBinding(binding);
    const kek = requireKey(this.keyring, this.keyring.activeKeyId);
    const dek = randomBytes(KEY_BYTES);
    const payloadBytes = Buffer.from(JSON.stringify(value), "utf8");

    try {
      return {
        version: 1,
        algorithm: "AES-256-GCM",
        keyId: this.keyring.activeKeyId,
        wrappedDek: encryptPart(kek, dek, additionalData(binding, "dek")),
        payload: encryptPart(dek, payloadBytes, additionalData(binding, "payload")),
      };
    } finally {
      dek.fill(0);
      payloadBytes.fill(0);
    }
  }

  decryptJson<T = unknown>(binding: SecretBinding, envelope: SecretEnvelopeV1): T {
    validateBinding(binding);
    validateEnvelope(envelope);
    const kek = requireKey(this.keyring, envelope.keyId);
    const dek = decryptPart(kek, envelope.wrappedDek, additionalData(binding, "dek"));

    try {
      const plaintext = decryptPart(dek, envelope.payload, additionalData(binding, "payload"));
      try {
        return JSON.parse(plaintext.toString("utf8")) as T;
      } catch (error) {
        throw new Error("secret envelope payload is invalid", { cause: error });
      } finally {
        plaintext.fill(0);
      }
    } finally {
      dek.fill(0);
    }
  }

  rewrap(binding: SecretBinding, envelope: SecretEnvelopeV1): SecretEnvelopeV1 {
    validateBinding(binding);
    validateEnvelope(envelope);
    const previousKek = requireKey(this.keyring, envelope.keyId);
    const activeKek = requireKey(this.keyring, this.keyring.activeKeyId);
    const dek = decryptPart(previousKek, envelope.wrappedDek, additionalData(binding, "dek"));

    try {
      return {
        ...envelope,
        keyId: this.keyring.activeKeyId,
        wrappedDek: encryptPart(activeKek, dek, additionalData(binding, "dek")),
      };
    } finally {
      dek.fill(0);
    }
  }
}

function parseKeyring(source: string): LocalSecretKeyring {
  let value: unknown;
  try {
    value = JSON.parse(source) as unknown;
  } catch (error) {
    throw new Error("invalid local secret keyring", { cause: error });
  }

  if (!isRecord(value) || value["version"] !== 1 || !isNonEmptyString(value["activeKeyId"]) ||
      !isRecord(value["keys"])) {
    throw new Error("invalid local secret keyring");
  }

  const keys = new Map<string, Buffer>();
  for (const [keyId, encodedKey] of Object.entries(value["keys"])) {
    if (!isNonEmptyString(keyId) || !isNonEmptyString(encodedKey)) {
      throw new Error("invalid local secret keyring");
    }
    const key = decodeBase64(encodedKey, "invalid local secret keyring");
    if (key.byteLength !== KEY_BYTES) {
      throw new Error("invalid local secret keyring");
    }
    keys.set(keyId, key);
  }

  const activeKeyId = value["activeKeyId"];
  if (!keys.has(activeKeyId)) {
    throw new Error("local secret keyring active key is unavailable");
  }
  return { activeKeyId, keys };
}

function encryptPart(key: Buffer, plaintext: Buffer, aad: Buffer): EncryptedPart {
  const nonce = randomBytes(NONCE_BYTES);
  const cipher = createCipheriv("aes-256-gcm", key, nonce, { authTagLength: AUTH_TAG_BYTES });
  cipher.setAAD(aad);
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  const authTag = cipher.getAuthTag();
  return {
    nonce: nonce.toString("base64"),
    ciphertext: ciphertext.toString("base64"),
    authTag: authTag.toString("base64"),
  };
}

function decryptPart(key: Buffer, encrypted: EncryptedPart, aad: Buffer): Buffer {
  try {
    const nonce = decodeBase64(encrypted.nonce, "secret envelope is malformed");
    const ciphertext = decodeBase64(encrypted.ciphertext, "secret envelope is malformed");
    const authTag = decodeBase64(encrypted.authTag, "secret envelope is malformed");
    if (nonce.byteLength !== NONCE_BYTES || authTag.byteLength !== AUTH_TAG_BYTES) {
      throw new Error("secret envelope is malformed");
    }
    const decipher = createDecipheriv("aes-256-gcm", key, nonce, { authTagLength: AUTH_TAG_BYTES });
    decipher.setAAD(aad);
    decipher.setAuthTag(authTag);
    return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
  } catch (error) {
    if (error instanceof Error && error.message === "secret envelope is malformed") {
      throw error;
    }
    throw new Error("secret envelope authentication failed", { cause: error });
  }
}

function additionalData(binding: SecretBinding, layer: "dek" | "payload"): Buffer {
  return Buffer.from(JSON.stringify([
    "cph-secret-envelope",
    1,
    layer,
    binding.purpose,
    binding.organizationId,
    binding.connectionId,
    binding.secretVersionId,
  ]), "utf8");
}

function requireKey(keyring: LocalSecretKeyring, keyId: string): Buffer {
  const key = keyring.keys.get(keyId);
  if (key === undefined) {
    throw new Error("secret envelope key is unavailable");
  }
  return key;
}

function validateBinding(binding: SecretBinding): void {
  if (!isNonEmptyString(binding.purpose) || !isNonEmptyString(binding.organizationId) ||
      !isNonEmptyString(binding.connectionId) || !isNonEmptyString(binding.secretVersionId)) {
    throw new Error("secret envelope binding is invalid");
  }
}

function validateEnvelope(envelope: SecretEnvelopeV1): void {
  if (!isRecord(envelope) || envelope["version"] !== 1 || envelope["algorithm"] !== "AES-256-GCM" ||
      !isNonEmptyString(envelope["keyId"]) || !isEncryptedPart(envelope["wrappedDek"]) ||
      !isEncryptedPart(envelope["payload"])) {
    throw new Error("secret envelope is malformed");
  }
}

function isEncryptedPart(value: unknown): value is EncryptedPart {
  return isRecord(value) && isNonEmptyString(value["nonce"]) &&
    isNonEmptyString(value["ciphertext"]) && isNonEmptyString(value["authTag"]);
}

function decodeBase64(value: string, errorMessage: string): Buffer {
  if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
    throw new Error(errorMessage);
  }
  const decoded = Buffer.from(value, "base64");
  if (decoded.toString("base64") !== value) {
    throw new Error(errorMessage);
  }
  return decoded;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function isNonEmptyString(value: unknown): value is string {
  return typeof value === "string" && value.trim() !== "";
}
