import {
  createCipheriv,
  createDecipheriv,
  createHash,
  createHmac,
  randomBytes,
  timingSafeEqual,
} from "node:crypto";
import { ServiceUnavailableError } from "@/lib/api/errors";

export interface EncryptedSecret {
  version: 1;
  algorithm: "aes-256-gcm";
  iv: string;
  ciphertext: string;
  authTag: string;
}

function encryptionKey() {
  const secret = process.env.MESSAGING_ENCRYPTION_KEY;
  if (!secret || secret.length < 32) {
    throw new ServiceUnavailableError(
      "Messaging encryption is not configured",
      undefined,
      "MESSAGING_ENCRYPTION_NOT_CONFIGURED",
    );
  }
  return createHash("sha256").update(secret, "utf8").digest();
}

export function encryptMessagingSecret(plaintext: string): EncryptedSecret {
  if (!plaintext.trim()) throw new Error("Secret cannot be empty");
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", encryptionKey(), iv);
  const ciphertext = Buffer.concat([
    cipher.update(plaintext, "utf8"),
    cipher.final(),
  ]);
  return {
    version: 1,
    algorithm: "aes-256-gcm",
    iv: iv.toString("base64url"),
    ciphertext: ciphertext.toString("base64url"),
    authTag: cipher.getAuthTag().toString("base64url"),
  };
}

export function decryptMessagingSecret(
  secret: EncryptedSecret | undefined,
): string {
  // A revoked connection keeps its `_id` but not its credential, so every
  // caller has to face the absent case rather than dereferencing undefined.
  if (!secret) {
    throw new Error("This channel connection no longer stores a credential");
  }
  if (secret.version !== 1 || secret.algorithm !== "aes-256-gcm") {
    throw new Error("Unsupported encrypted secret");
  }
  const decipher = createDecipheriv(
    "aes-256-gcm",
    encryptionKey(),
    Buffer.from(secret.iv, "base64url"),
  );
  decipher.setAuthTag(Buffer.from(secret.authTag, "base64url"));
  return Buffer.concat([
    decipher.update(Buffer.from(secret.ciphertext, "base64url")),
    decipher.final(),
  ]).toString("utf8");
}

export function verifyMetaWebhookSignature(rawBody: string, signature?: string) {
  const appSecret = process.env.META_APP_SECRET;
  if (!appSecret || !signature?.startsWith("sha256=")) return false;
  const expected = createHmac("sha256", appSecret)
    .update(rawBody, "utf8")
    .digest("hex");
  const received = signature.slice("sha256=".length).toLowerCase();
  if (!/^[a-f0-9]{64}$/.test(received)) return false;
  return timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(received, "hex"),
  );
}

interface MetaOnboardingState {
  userId: string;
  expiresAt: number;
  nonce: string;
}

function metaAppSecret() {
  const secret = process.env.META_APP_SECRET?.trim();
  if (!secret) {
    throw new ServiceUnavailableError(
      "META_APP_SECRET is not configured",
      undefined,
      "META_APP_SECRET_NOT_CONFIGURED",
    );
  }
  return secret;
}

/**
 * Mints a Telegram webhook secret and its stored hash.
 *
 * Only the hash is persisted; the plaintext goes to Telegram's setWebhook and
 * is then discarded, so the stored value cannot be replayed as a webhook.
 */
export function createTelegramWebhookSecret() {
  // Telegram restricts this to 1-256 chars of A-Z a-z 0-9 _ and -.
  const secret = randomBytes(32).toString("base64url");
  return { secret, hash: hashTelegramWebhookSecret(secret) };
}

export function hashTelegramWebhookSecret(secret: string) {
  return createHash("sha256").update(secret).digest("hex");
}

export function createMetaOnboardingState(userId: string) {
  const payload: MetaOnboardingState = {
    userId,
    expiresAt: Date.now() + 10 * 60 * 1000,
    nonce: randomBytes(18).toString("base64url"),
  };
  const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString(
    "base64url",
  );
  const signature = createHmac("sha256", metaAppSecret())
    .update(encoded, "utf8")
    .digest("base64url");
  return `${encoded}.${signature}`;
}

export function verifyMetaOnboardingState(
  token: string,
  expectedUserId: string,
) {
  const [encoded, received] = token.split(".");
  if (!encoded || !received) return false;
  const expected = createHmac("sha256", metaAppSecret())
    .update(encoded, "utf8")
    .digest();
  let receivedBuffer: Buffer;
  try {
    receivedBuffer = Buffer.from(received, "base64url");
  } catch {
    return false;
  }
  if (
    receivedBuffer.length !== expected.length ||
    !timingSafeEqual(expected, receivedBuffer)
  ) {
    return false;
  }
  try {
    const payload = JSON.parse(
      Buffer.from(encoded, "base64url").toString("utf8"),
    ) as MetaOnboardingState;
    return (
      payload.userId === expectedUserId &&
      Number.isFinite(payload.expiresAt) &&
      payload.expiresAt >= Date.now() &&
      typeof payload.nonce === "string" &&
      payload.nonce.length >= 20
    );
  } catch {
    return false;
  }
}
