import { env } from "@/lib/env";
import { createMiddleware } from "hono/factory";
import * as crypto from "node:crypto";

/**
 * Service-to-service auth for the /v1/internal/* route groups.
 *
 * Two accepted credentials, checked in this order:
 *
 *   Authorization  "Bearer <INTERNAL_SERVER_API_KEY>", or the bare key. The
 *   / x-api-key     conventional form, usable from curl, Postman, a shell
 *                  script. No replay protection: whoever can read the header
 *                  has the key itself.
 *
 *   x-api-payload  AES-256-GCM envelope carrying the key and a timestamp. The
 *                  key never appears in the header, and a captured request is
 *                  useless after 60 seconds. Used by the SMTP worker and the
 *                  VS Code extension.
 *
 * The envelope is the stronger of the two and stays the default for automated
 * callers. x-api-key exists because the envelope is impractical to mint by hand
 * and made the endpoints untestable with ordinary tools.
 *
 * Both are only as safe as the transport. Over plain HTTP the raw key is
 * readable by anything on the path; the envelope at least keeps the key itself
 * out of the header. Terminate TLS in front of this in any deployed environment.
 */

/** Length-independent, constant-time string compare. */
const safeEqual = (a: string, b: string): boolean => {
  const bufA = Buffer.from(a, "utf-8");
  const bufB = Buffer.from(b, "utf-8");
  // timingSafeEqual throws on length mismatch, and returning early on that
  // would leak the key's length. Hash first so the comparison is always over
  // two equal-length digests.
  const hashA = crypto.createHash("sha256").update(bufA).digest();
  const hashB = crypto.createHash("sha256").update(bufB).digest();
  return crypto.timingSafeEqual(hashA, hashB);
};

export const apiKeyAuth = () =>
  createMiddleware(async (c, next) => {
    const secret = env.GetString("INTERNAL_SERVER_API_KEY");

    if (!secret) {
      console.error("FATAL: INTERNAL_SERVER_API_KEY is not set on the server.");
      return c.text("Server configuration error", 500);
    }

    // ── plain key ─────────────────────────────────────────────────────────
    //
    // Authorization is the conventional place for a credential, so it wins.
    // x-api-key stays accepted because callers already use it.
    //
    // "Bearer " is stripped when present but not required: a bare key in
    // Authorization is the mistake people actually make, and rejecting it would
    // look like a wrong key rather than a wrong format.
    const authHeader = c.req.header("authorization");
    const rawKey =
      (authHeader ? authHeader.replace(/^Bearer\s+/i, "") : undefined) ??
      c.req.header("x-api-key");

    if (rawKey) {
      if (safeEqual(rawKey.trim(), secret)) {
        await next();
        return;
      }
      return c.text("Invalid API key", 401);
    }

    // ── encrypted envelope ────────────────────────────────────────────────
    const payload = c.req.header("x-api-payload");
    if (!payload) {
      return c.text(
        "Unauthorized: send Authorization: Bearer <key>, x-api-key, or x-api-payload",
        401,
      );
    }

    try {
      const [ivHex, cipherHex] = payload.split(":");
      if (!ivHex || !cipherHex) {
        // Almost always someone passing the raw key in the wrong header, so say
        // that rather than letting it surface as a generic decryption failure.
        throw new Error(
          'expected "<iv hex>:<ciphertext hex>" — to send the key directly, use the x-api-key header instead',
        );
      }

      const cipherRaw = Buffer.from(cipherHex, "hex");

      const tag = cipherRaw.subarray(cipherRaw.length - 16);
      const data = cipherRaw.subarray(0, cipherRaw.length - 16);

      const keyBuffer = Buffer.from(secret, "utf-8");
      const ivBuffer = Buffer.from(ivHex, "hex");

      const decipher = crypto.createDecipheriv(
        "aes-256-gcm",
        keyBuffer,
        ivBuffer,
      );
      decipher.setAuthTag(tag);

      const decryptedText =
        decipher.update(data, undefined, "utf8") + decipher.final("utf8");
      const decrypted = JSON.parse(decryptedText);

      if (decrypted.key !== secret)
        throw new Error("Invalid API key payload secret");
      if (Date.now() - decrypted.ts > 60000)
        return c.text("Token Expired", 401);
    } catch (e) {
      console.error("Decryption Error:", (e as Error).message);
      return c.text(`Invalid Security Payload: ${(e as Error).message}`, 401);
    }
    await next();
  });
