// outcome contract for audit Task input persistence.
//
// Doctrine: every audit Task records enough operator-meaningful information to
// explain what happened, AND no audit Task contains secret material. Mechanism
// is centralised here, not at the per-kind writer: form/action input
// definitions tag secret fields at the source of truth, and writers strip
// those fields via `redactSecrets` before persisting `inputs.<field>` props
// onto the Task node.
//
// Single file by design — no per-kind branching, no factory. A future
// schema shape change extends `FormSchema` here; the contract stays one place.

export type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue };

export interface FormSchema {
  /**
   * Field names whose values are secret and must never land on a Task node.
   * Declared at the form definition (e.g. `cloudflare-setup-form`'s schema in
   * `cloudflare-setup-types.ts`), not at the writer. Writers pass the form
   * schema through unmodified — the contract is "secrets are declared once
   * where the form is defined, redaction is enforced once at write time."
   */
  secretFields?: string[];
}

export interface RedactResult {
  /**
   * Payload with every `secretFields` entry removed. Non-secret keys keep
   * their values intact (including null, false, 0, empty string). Keys not
   * mentioned in `secretFields` always pass through — secrets are an
   * allow-list of names to drop, not the entire universe.
   */
  redacted: Record<string, JsonValue>;
  /** Number of fields that were stripped. Callers log this when > 0. */
  droppedFields: number;
  /** Names of fields that were stripped, sorted for stable log lines. */
  droppedFieldNames: string[];
}

/**
 * Strip every secret-tagged field from `payload` per `schema`. Pure: no IO,
 * no logging, no thrown errors. Caller is responsible for the
 * `[task] redacted ...` log line when `droppedFields > 0`.
 *
 * Edge-case behaviour:
 * - `payload` undefined / null → empty result, droppedFields=0.
 * - `schema` undefined / no `secretFields` → every key passes through.
 * - A secret field whose key is absent from `payload` → no-op (not counted).
 * - Non-string values on a secret field → key still dropped, value never
 *   inspected. Redaction is by KEY, not by value-shape heuristic.
 */
export function redactSecrets(
  payload: Record<string, JsonValue> | null | undefined,
  schema: FormSchema | null | undefined,
): RedactResult {
  if (!payload) {
    return { redacted: {}, droppedFields: 0, droppedFieldNames: [] };
  }
  const secrets = new Set(schema?.secretFields ?? []);
  const redacted: Record<string, JsonValue> = {};
  const dropped: string[] = [];
  for (const [key, value] of Object.entries(payload)) {
    if (secrets.has(key)) {
      dropped.push(key);
      continue;
    }
    redacted[key] = value;
  }
  dropped.sort();
  return { redacted, droppedFields: dropped.length, droppedFieldNames: dropped };
}
