import { z } from "zod"; export const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/).refine(value => { const date = new Date(`${value}T00:00:00Z`); return !Number.isNaN(+date) && date.toISOString().slice(0, 10) === value && value >= '1970-01-01' && value <= '9998-12-31'; }, 'Expected a real calendar date from 1970 through 9998'); const name = z.string().trim().min(1).max(200); export const scheduleSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('daily') }).strict(), z.object({ type: z.literal('interval'), every: z.number().int().min(1).max(3650), anchor: dateSchema }).strict(), z.object({ type: z.literal('weekdays'), days: z.array(z.number().int().min(0).max(6)).min(1).max(7).refine(a => new Set(a).size === a.length) }).strict(), z.object({ type: z.literal('weekly'), every: z.number().int().min(1).max(520), weekday: z.number().int().min(0).max(6), anchor: dateSchema }).strict(), ]); export type Schedule = z.infer; const schedule = scheduleSchema.default({ type: 'daily' }); export const taskInput = z.object({ name, schedule }).strict(); export const taskPatch = taskInput.partial().refine(v => Object.keys(v).length > 0); const common = { name, schedule }; export const habitInput = z.discriminatedUnion('method', [ z.object({ ...common, method: z.literal('count'), carryPartialProgress: z.boolean().default(false), target: z.number().int().min(1).max(10000), unit: z.string().trim().min(1).max(80).default('steps') }).strict(), z.object({ ...common, method: z.literal('manual') }).strict(), z.object({ ...common, method: z.literal('tasks'), tasks: z.array(taskInput).max(100).default([]) }).strict(), ]); export type HabitConfig = { name: string; schedule: Schedule; archived: boolean } & ( { method: 'count'; carryPartialProgress: boolean; target: number; unit: string } | { method: 'manual' } | { method: 'tasks'; tasks: { id: string; name: string; schedule: Schedule }[] } ); export const habitPatch = z.object({ carryPartialProgress: z.boolean().optional(), name: name.optional(), schedule: scheduleSchema.optional(), method: z.enum(['count', 'manual', 'tasks']).optional(), target: z.number().int().min(1).max(10000).optional(), unit: z.string().trim().min(1).max(80).optional(), archived: z.boolean().optional() }).strict().refine(v => Object.keys(v).length > 0); export const progressInput = z.union([z.object({ count: z.number().int().min(0).max(1_000_000_000) }).strict(), z.object({ done: z.boolean() }).strict()]); export const doneInput = z.object({ done: z.boolean() }).strict(); const color = z.string().regex(/^#[0-9a-fA-F]{6}$/, 'Use a six-digit hex color'); // Product preferences only; layout and callbacks belong to the future UI. export const calendarSettingsSchema = z.object({ mainColor: color.default('#196127'), shadeCount: z.union([z.literal('auto'), z.number().int().min(2).max(20)]).default('auto'), emptyColor: color.default('#ebedf0'), notDueColor: color.default('#f5f5f5'), futureColor: color.default('#dbeafe'), }).strict(); export type CalendarSettings = z.infer; export const chartInput = z.object({ name, habitIds: z.array(z.string().uuid()).min(1).max(100).refine(a => new Set(a).size === a.length), settings: calendarSettingsSchema.default(() => calendarSettingsSchema.parse({})) }).strict(); export const chartPatch = chartInput.partial().refine(v => Object.keys(v).length > 0); export const timezoneInput = z.object({ timezone: z.string().min(1).max(100).refine(v => { try { return !/^[+-]/.test(v) && !!new Intl.DateTimeFormat('en', { timeZone: v }); } catch { return false; } }) }).strict();