import {
  SUPER_SYNC_CLIENT_ID_REGEX,
  SUPER_SYNC_MAX_CLIENT_ID_LENGTH,
} from '@sp/shared-schema';

export {
  SUPER_SYNC_CLIENT_ID_REGEX as CLIENT_ID_REGEX,
  SUPER_SYNC_MAX_CLIENT_ID_LENGTH as MAX_CLIENT_ID_LENGTH,
} from '@sp/shared-schema';

/**
 * Type-guard for clientId validation. Order matters: cheap length check first
 * so an attacker passing a multi-megabyte clientId is rejected before the
 * regex scans it. The regex already requires `+` (≥1 char), so an explicit
 * non-empty check is redundant. Used by the WS route handler AND the
 * rate-limit keyGenerator — keep both call sites in sync via this helper.
 */
export const isValidClientId = (cid: unknown): cid is string =>
  typeof cid === 'string' &&
  cid.length <= SUPER_SYNC_MAX_CLIENT_ID_LENGTH &&
  SUPER_SYNC_CLIENT_ID_REGEX.test(cid);

/**
 * Approximate bytes-per-op used when decrementing `users.storage_used_bytes`
 * during DELTA-op cleanup-deletes. ONLY valid for ordinary CRT/UPD/DEL ops
 * whose payloads observably cluster around 150-300 bytes — picking 1024 is a
 * conservative over-estimate so the cleanup loop reliably makes progress;
 * drift is reconciled once at the end of `freeStorageForUpload` via a single
 * `updateStorageUsage` scan.
 *
 * DO NOT use for full-state ops (SYNC_IMPORT / BACKUP_IMPORT / REPAIR). Their
 * payloads can be up to 20MB, so 1024 undercounts by ~20000x and the cached
 * counter ends up permanently low if reconcile fails. `deleteOldestRestorePointAndOps`
 * measures the exact `pg_column_size(payload)` for those 1-2 rows BEFORE
 * deleting; the persisted payload_bytes value avoids reintroducing the
 * SUM(pg_column_size) DoS that scanning every delta op caused.
 */
export const APPROX_BYTES_PER_OP = 1024;

/**
 * Locally-computed approximation of how many bytes an operation's payload and
 * vector clock will occupy on disk. Used by both the route layer (for quota
 * gating and post-commit counter deltas) and the service layer (for the atomic
 * counter write inside the upload transaction). Keeping a single
 * implementation guarantees the gate, the operation payload_bytes column, and
 * the increment cannot disagree about what "size" means.
 *
 * Robust against malformed payloads: if JSON.stringify throws (e.g. BigInt,
 * circular ref), the op is charged APPROX_BYTES_PER_OP so the counter cannot
 * be bypassed by submitting unserializable ops that still persist as JSONB.
 * `fallback` is `true` in that case so callers can observe the rate of
 * unserializable ops via a single log line (never the op content).
 *
 * `cachedPayloadBytes` lets a caller pass the payload's UTF-8 byte size when it
 * was already measured upstream (validation stringifies the payload to enforce
 * the size limit; the payload is immutable across the upload pipeline), so a
 * multi-megabyte payload isn't re-stringified here. The vector clock is always
 * (re)measured because it is pruned AFTER validation (see
 * `limitVectorClockSize` / `pruneVectorClockForStorage`) — the stored clock
 * differs from the validation/gate-time clock, so its size must be computed at
 * the persist site.
 */
export const computeOpStorageBytes = (
  op: {
    payload: unknown;
    vectorClock: unknown;
  },
  cachedPayloadBytes?: number,
): { bytes: number; fallback: boolean } => {
  try {
    const payloadBytes =
      cachedPayloadBytes ?? Buffer.byteLength(JSON.stringify(op.payload ?? null), 'utf8');
    return {
      bytes:
        payloadBytes + Buffer.byteLength(JSON.stringify(op.vectorClock ?? {}), 'utf8'),
      fallback: false,
    };
  } catch {
    return { bytes: APPROX_BYTES_PER_OP, fallback: true };
  }
};
