import { z } from "zod";

/**
 * Bounded JSONB limits for untrusted attacker-controlled metadata / dynamicFields.
 *
 * A client can POST arbitrary JSON via `z.record(z.string(), z.unknown())`, which
 * — unbounded — lets a manager-role user ship payloads that consume DB / log /
 * memory disproportionately, and pollute namespaces read by admin tooling.
 *
 * These defaults are conservative: they cover ~99.9% of legitimate metadata and
 * deny the rest outright. Any call site that genuinely needs more should use its
 * own strict `z.object({...}).strict()` schema instead.
 */
export const BOUNDED_JSON_LIMITS = Object.freeze({
  MAX_KEY_LENGTH: 128,
  MAX_KEYS: 64,
  MAX_DEPTH: 4,
  MAX_STRING_LENGTH: 8_000,
  MAX_SERIALIZED_BYTES: 32_768,
});

function validateValue(value: unknown, depth: number, ctx: z.RefinementCtx): boolean {
  if (depth > BOUNDED_JSON_LIMITS.MAX_DEPTH) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: `Nested too deep (max depth ${BOUNDED_JSON_LIMITS.MAX_DEPTH})`,
    });
    return false;
  }
  if (value == null) return true;
  if (typeof value === "boolean" || typeof value === "number") return true;
  if (typeof value === "string") {
    if (value.length > BOUNDED_JSON_LIMITS.MAX_STRING_LENGTH) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `String exceeds max length ${BOUNDED_JSON_LIMITS.MAX_STRING_LENGTH}`,
      });
      return false;
    }
    return true;
  }
  if (Array.isArray(value)) {
    if (value.length > BOUNDED_JSON_LIMITS.MAX_KEYS) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `Array too large (max ${BOUNDED_JSON_LIMITS.MAX_KEYS} elements)`,
      });
      return false;
    }
    return value.every((v) => validateValue(v, depth + 1, ctx));
  }
  if (typeof value === "object") {
    const obj = value as Record<string, unknown>;
    const keys = Object.keys(obj);
    if (keys.length > BOUNDED_JSON_LIMITS.MAX_KEYS) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `Object has too many keys (max ${BOUNDED_JSON_LIMITS.MAX_KEYS})`,
      });
      return false;
    }
    for (const k of keys) {
      if (k.length > BOUNDED_JSON_LIMITS.MAX_KEY_LENGTH) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: `Key exceeds max length ${BOUNDED_JSON_LIMITS.MAX_KEY_LENGTH}`,
        });
        return false;
      }
      if (!validateValue(obj[k], depth + 1, ctx)) return false;
    }
    return true;
  }
  ctx.addIssue({
    code: z.ZodIssueCode.custom,
    message: `Unsupported JSON type: ${typeof value}`,
  });
  return false;
}

/**
 * A Zod schema for attacker-controlled JSONB payloads (metadata, dynamicFields).
 *
 * Limits key count, nesting depth, string length, and total serialized byte size
 * (see BOUNDED_JSON_LIMITS). Use in place of `z.record(z.string(), z.unknown())`
 * wherever the input is user-submitted.
 */
export const BoundedJsonRecord = z.record(z.string(), z.unknown()).superRefine((val, ctx) => {
  const keys = Object.keys(val);
  if (keys.length > BOUNDED_JSON_LIMITS.MAX_KEYS) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: `Object has too many keys (max ${BOUNDED_JSON_LIMITS.MAX_KEYS})`,
    });
    return;
  }
  for (const k of keys) {
    if (k.length > BOUNDED_JSON_LIMITS.MAX_KEY_LENGTH) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `Key exceeds max length ${BOUNDED_JSON_LIMITS.MAX_KEY_LENGTH}`,
      });
      return;
    }
    if (!validateValue(val[k], 1, ctx)) return;
  }
  try {
    const serialized = JSON.stringify(val);
    if (Buffer.byteLength(serialized, "utf8") > BOUNDED_JSON_LIMITS.MAX_SERIALIZED_BYTES) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `Serialized payload exceeds max size ${BOUNDED_JSON_LIMITS.MAX_SERIALIZED_BYTES} bytes`,
      });
    }
  } catch {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Payload is not JSON-serializable",
    });
  }
});

/** Helper that returns a fresh BoundedJsonRecord (so callers can `.default({})` etc. independently). */
export function boundedJsonRecord() {
  return BoundedJsonRecord;
}
