import { z } from 'zod';

/**
 * Auditor T1D (2026-06-01): /api/executive/tokens body validation.
 *
 * POST creates a long-lived display token. Prior code did a destructure
 * with only `if (!displayName)`. Without Zod, an attacker could push a
 * 10 MB displayName into the DB (no length cap), CR/LF-inject into
 * any UI that renders the field, or pass non-numeric countryId.
 */
const noControlChars = (val: string) => !/[\r\n\t\0]/.test(val);
const noControlCharsMsg = 'Control characters (CR, LF, tab, NUL) are not allowed';

export const createExecTokenSchema = z.object({
    displayName: z.string()
        .min(1, 'Display name is required')
        .max(120)
        .refine(noControlChars, noControlCharsMsg),
    countryId: z.coerce.number().int().positive().optional().nullable(),
    // "Lifetime (Never Expires)" in the UI submits 0; the route treats
    // 0/absent as no expiry (expiresAt = null). Normalize the lifetime
    // sentinel (0 / "0" / "" / null) to undefined so `.optional()` applies,
    // while still validating a real 1–3650 day window.
    expiryDays: z.preprocess(
        (v) => (v === 0 || v === '0' || v === '' || v === null ? undefined : v),
        z.coerce.number().int().min(1).max(3650).optional(),
    ),
    deviceName: z.string().max(200).refine(noControlChars, noControlCharsMsg).optional().nullable(),
});

/**
 * DELETE body is either `{ action: 'clear_all' }` OR `{ tokenId: N }`.
 * Modeled as a discriminated union so each branch's required fields are
 * enforced.
 */
export const deleteExecTokenSchema = z.union([
    z.object({ action: z.literal('clear_all') }),
    z.object({ tokenId: z.coerce.number().int().positive() }),
]);

export type CreateExecTokenInput = z.infer<typeof createExecTokenSchema>;
export type DeleteExecTokenInput = z.infer<typeof deleteExecTokenSchema>;
