import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { ChannelThreadToken } from "../data/entities.js";
import { isUniqueViolation } from "./pg-errors.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("communication_channels").child({ component: "thread-token" });
const TOKEN_PREFIX = "om_";
const RANDOM_BYTES = 16;
const HMAC_BYTES = 8;
const HMAC_KEY_ENV = "OM_THREAD_TOKEN_SECRET";
const HMAC_FALLBACK_KEY_ENV = "KMS_MASTER_KEY";
const HMAC_KEY_INFO = "thread-token";
const TOKEN_REGEX = /om_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{11}/;
let cachedKey = null;
function getKey() {
  if (cachedKey) return cachedKey;
  const primary = process.env[HMAC_KEY_ENV];
  if (primary && primary.length > 0) {
    cachedKey = Buffer.from(primary, "utf8");
    return cachedKey;
  }
  const fallback = process.env[HMAC_FALLBACK_KEY_ENV];
  if (fallback && fallback.length > 0) {
    cachedKey = createHmac("sha256", fallback).update(HMAC_KEY_INFO).digest();
    return cachedKey;
  }
  if (process.env.NODE_ENV === "production") {
    throw new Error(
      `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured \u2014 refusing to sign thread tokens with a static dev key in production.`
    );
  }
  logger.warn(
    `No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured. Thread tokens will use a dev-only static key \u2014 DO NOT USE IN PRODUCTION.`
  );
  cachedKey = createHash("sha256").update("open-mercato-thread-token-dev").digest();
  return cachedKey;
}
function _resetThreadTokenKeyCache() {
  cachedKey = null;
}
function base64urlEncode(buf) {
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
function base64urlDecode(value) {
  if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
  const padded = value + "=".repeat((4 - value.length % 4) % 4);
  try {
    return Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64");
  } catch {
    return null;
  }
}
function computeHmacBytes(random) {
  return createHmac("sha256", getKey()).update(random).digest().subarray(0, HMAC_BYTES);
}
function generateToken() {
  const random = randomBytes(RANDOM_BYTES);
  const hmac = computeHmacBytes(random);
  return `${TOKEN_PREFIX}${base64urlEncode(random)}_${base64urlEncode(hmac)}`;
}
const RANDOM_B64_LEN = Math.ceil(RANDOM_BYTES * 4 / 3);
const HMAC_B64_LEN = Math.ceil(HMAC_BYTES * 4 / 3);
const TOKEN_TOTAL_LEN = TOKEN_PREFIX.length + RANDOM_B64_LEN + 1 + HMAC_B64_LEN;
function verifyToken(token) {
  if (typeof token !== "string") return false;
  if (token.length !== TOKEN_TOTAL_LEN) return false;
  if (!token.startsWith(TOKEN_PREFIX)) return false;
  const randomStart = TOKEN_PREFIX.length;
  const randomEnd = randomStart + RANDOM_B64_LEN;
  const separator = token[randomEnd];
  if (separator !== "_") return false;
  const hmacStart = randomEnd + 1;
  const randomPart = token.slice(randomStart, randomEnd);
  const hmacPart = token.slice(hmacStart, hmacStart + HMAC_B64_LEN);
  const random = base64urlDecode(randomPart);
  if (!random || random.length !== RANDOM_BYTES) return false;
  const provided = base64urlDecode(hmacPart);
  if (!provided || provided.length !== HMAC_BYTES) return false;
  const expected = computeHmacBytes(random);
  try {
    return timingSafeEqual(provided, expected);
  } catch {
    return false;
  }
}
function buildReferencesId(token) {
  return `<${token}@open-mercato.invalid>`;
}
function buildBodyFooter(token) {
  return {
    html: `<span style="display:none">[OM:${token}]</span>`,
    plain: `

[OM:${token}]`
  };
}
function applyOutboundThreadingToken(payload, token) {
  if (!verifyToken(token)) {
    throw new Error("applyOutboundThreadingToken: invalid token format/signature");
  }
  const refId = buildReferencesId(token);
  const footer = buildBodyFooter(token);
  const headers = { ...payload.headers ?? {} };
  const existingRefs = headers["references"] ?? headers["References"];
  let nextRefs;
  if (Array.isArray(existingRefs)) {
    nextRefs = existingRefs.includes(refId) ? existingRefs.join(" ") : [...existingRefs, refId].join(" ");
  } else if (typeof existingRefs === "string" && existingRefs.length > 0) {
    nextRefs = existingRefs.includes(refId) ? existingRefs : `${existingRefs} ${refId}`;
  } else {
    nextRefs = refId;
  }
  delete headers["references"];
  headers["References"] = nextRefs;
  let bodyHtml = payload.bodyHtml;
  if (typeof bodyHtml === "string") {
    if (!bodyHtml.includes(`[OM:${token}]`)) {
      const closing = bodyHtml.lastIndexOf("</body>");
      if (closing >= 0) {
        bodyHtml = `${bodyHtml.slice(0, closing)}${footer.html}${bodyHtml.slice(closing)}`;
      } else {
        bodyHtml = `${bodyHtml}${footer.html}`;
      }
    }
  }
  let bodyText = payload.bodyText;
  if (typeof bodyText === "string") {
    if (!bodyText.includes(`[OM:${token}]`)) {
      bodyText = `${bodyText}${footer.plain}`;
    }
  }
  return {
    ...payload,
    headers,
    ...bodyHtml !== void 0 ? { bodyHtml } : {},
    ...bodyText !== void 0 ? { bodyText } : {}
  };
}
function extractTokenFromHeaders(inReplyTo, references) {
  const haystack = [];
  if (typeof inReplyTo === "string" && inReplyTo.length > 0) haystack.push(inReplyTo);
  if (Array.isArray(references)) haystack.push(...references);
  else if (typeof references === "string" && references.length > 0) haystack.push(references);
  for (const candidate of haystack) {
    const matches = candidate.match(new RegExp(TOKEN_REGEX, "g"));
    if (!matches) continue;
    for (const match of matches) {
      if (verifyToken(match)) return match;
    }
  }
  return null;
}
async function getOrCreateThreadToken(em, args) {
  const dscope = { tenantId: args.tenantId, organizationId: args.organizationId };
  const existing = await findOneWithDecryption(
    em,
    ChannelThreadToken,
    {
      tenantId: args.tenantId,
      organizationId: args.organizationId,
      messageThreadId: args.messageThreadId
    },
    void 0,
    dscope
  );
  if (existing) {
    return { token: existing.token, created: false };
  }
  const row = em.create(ChannelThreadToken, {
    tenantId: args.tenantId,
    organizationId: args.organizationId,
    messageThreadId: args.messageThreadId,
    token: generateToken()
  });
  em.persist(row);
  try {
    await em.flush();
    return { token: row.token, created: true };
  } catch (err) {
    if (!isUniqueViolation(err)) throw err;
    const winner = await findOneWithDecryption(
      em.fork(),
      ChannelThreadToken,
      {
        tenantId: args.tenantId,
        organizationId: args.organizationId,
        messageThreadId: args.messageThreadId
      },
      void 0,
      dscope
    );
    if (winner) return { token: winner.token, created: false };
    throw err;
  }
}
function extractTokenFromBody(bodyHtml, bodyText) {
  const haystacks = [bodyHtml, bodyText].filter(
    (value) => typeof value === "string" && value.length > 0
  );
  const pattern = new RegExp(`\\[OM:(${TOKEN_REGEX.source})\\]`, "g");
  for (const haystack of haystacks) {
    let match;
    pattern.lastIndex = 0;
    while ((match = pattern.exec(haystack)) !== null) {
      if (verifyToken(match[1])) return match[1];
    }
  }
  return null;
}
export {
  _resetThreadTokenKeyCache,
  applyOutboundThreadingToken,
  buildBodyFooter,
  buildReferencesId,
  extractTokenFromBody,
  extractTokenFromHeaders,
  generateToken,
  getOrCreateThreadToken,
  verifyToken
};
//# sourceMappingURL=thread-token.js.map
