import { z } from 'zod'; const coerceAbv = z.preprocess((val) => { if (val === null || val === undefined || val === '') return null; const n = Number(val); if (isNaN(n)) return null; return n; }, z.number().min(0).max(100).nullish()); const coerceAge = z.preprocess((val) => { if (val === null || val === undefined || val === '') return null; const n = Number(val); if (isNaN(n)) return null; // If it looks like a year (e.g. 19xx or 20xx), it's probably not the age if (n > 1900 && n < 2100) return null; return n; }, z.number().min(0).max(200).nullish()); export const BottleMetadataSchema = z.object({ name: z.string().trim().min(1).max(255).nullish(), distillery: z.string().trim().max(255).nullish(), bottler: z.string().trim().max(255).nullish(), series: z.string().trim().max(255).nullish(), category: z.string().trim().max(100).nullish(), abv: coerceAbv, age: coerceAge, vintage: z.string().trim().max(50).nullish(), bottleCode: z.string().trim().max(100).nullish(), whiskybaseId: z.string().trim().max(50).nullish(), distilled_at: z.string().trim().max(50).nullish(), bottled_at: z.string().trim().max(50).nullish(), batch_info: z.string().trim().max(255).nullish(), cask_type: z.string().trim().max(255).nullish(), is_whisky: z.boolean().default(true), confidence: z.number().min(0).max(100).default(100), confidence_scores: z.record(z.string(), z.number().min(0).max(100)).nullish(), purchase_price: z.number().min(0).nullish(), status: z.enum(['sealed', 'open', 'sampled', 'empty']).default('sealed').nullish(), suggested_tags: z.array(z.string().trim().max(100)).nullish(), suggested_custom_tags: z.array(z.string().trim().max(100)).nullish(), }); export type BottleMetadata = z.infer; export const TastingNoteSchema = z.object({ bottle_id: z.string().uuid(), session_id: z.string().uuid().optional(), rating: z.number().min(0).max(100), nose_notes: z.string().trim().max(2000).optional(), palate_notes: z.string().trim().max(2000).optional(), finish_notes: z.string().trim().max(2000).optional(), is_sample: z.boolean().optional().default(false), buddy_ids: z.array(z.string().uuid()).optional(), tag_ids: z.array(z.string().uuid()).optional(), tasted_at: z.string().datetime().optional(), blind_label: z.string().trim().max(100).nullish(), guess_abv: z.number().min(0).max(100).nullish(), guess_age: z.number().min(0).max(200).nullish(), guess_region: z.string().trim().max(100).nullish(), guess_points: z.number().min(0).nullish(), flavor_profile: z.object({ smoky: z.number().min(0).max(100), fruity: z.number().min(0).max(100), spicy: z.number().min(0).max(100), sweet: z.number().min(0).max(100), floral: z.number().min(0).max(100), }).optional(), }); export type TastingNoteData = z.infer; export const UpdateBottleSchema = z.object({ name: z.string().trim().min(1).max(255).nullish(), distillery: z.string().trim().max(255).nullish(), category: z.string().trim().max(100).nullish(), abv: coerceAbv, age: coerceAge, whiskybase_id: z.string().trim().max(50).nullish(), purchase_price: z.number().min(0).nullish(), distilled_at: z.string().trim().max(50).nullish(), bottled_at: z.string().trim().max(50).nullish(), batch_info: z.string().trim().max(255).nullish(), cask_type: z.string().trim().max(255).nullish(), status: z.enum(['sealed', 'open', 'sampled', 'empty']).nullish(), }); export type UpdateBottleData = z.infer; export const TagSchema = z.object({ name: z.string().trim().min(1).max(50), category: z.enum(['nose', 'taste', 'finish', 'texture']), }); export type TagData = z.infer; export const AdminCreditUpdateSchema = z.object({ userId: z.string().uuid(), newBalance: z.number().min(0).max(1000000), reason: z.string().trim().min(1).max(255), }); export type AdminCreditUpdateData = z.infer; export const AdminSettingsSchema = z.object({ userId: z.string().uuid(), dailyLimit: z.number().min(0).max(10000).nullable(), googleSearchCost: z.number().min(0).max(1000).optional(), geminiAiCost: z.number().min(0).max(1000).optional(), }); export type AdminSettingsData = z.infer; export const DiscoveryDataSchema = z.object({ name: z.string().trim().min(1).max(255), distillery: z.string().trim().max(255).nullish(), abv: coerceAbv, age: coerceAge, distilled_at: z.string().trim().max(50).nullish(), bottled_at: z.string().trim().max(50).nullish(), batch_info: z.string().trim().max(255).nullish(), cask_type: z.string().trim().max(255).nullish(), }); export type DiscoveryData = z.infer; export const BuddySchema = z.object({ name: z.string().trim().min(1).max(100), }); export type BuddyData = z.infer; export interface AnalysisResponse { success: boolean; data?: BottleMetadata; error?: string; isAiError?: boolean; imageHash?: string; perf?: { // Legacy fields (kept for backward compatibility) apiDuration?: number; parseDuration?: number; // Detailed metrics imagePrep?: number; cacheCheck?: number; encoding?: number; modelInit?: number; apiCall?: number; parsing?: number; validation?: number; dbOps?: number; uploadSize: number; total?: number; cacheHit?: boolean; }; raw?: any; }