import type { AvailabilityMode, AvailabilityRange, DateTimeInput, MemorySpace, ScheduleBlock, ScheduleEntry, ScheduledAvailabilitySnapshot, } from './types'; const DAY_MS = 24 * 60 * 60 * 1000; export function toDate(input: DateTimeInput): Date { const date = input instanceof Date ? new Date(input.getTime()) : new Date(input); if (Number.isNaN(date.getTime())) { throw new Error(`Invalid datetime: ${String(input)}`); } return date; } export function toIso(input: DateTimeInput): string { return toDate(input).toISOString(); } export function startOfUtcDay(input: DateTimeInput): Date { const date = toDate(input); return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); } export function addUtcDays(input: DateTimeInput, days: number): Date { return new Date(startOfUtcDay(input).getTime() + days * DAY_MS); } export function scheduleTargetDay(now: DateTimeInput): Date { return addUtcDays(now, 1); } export function daysInMonth(date: Date): number { const year = date.getUTCFullYear(); const month = date.getUTCMonth(); return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); } export function scheduleInstruction(): string { return [ 'Generate realistic schedule blocks for the persona based on their profile and the provided message.', 'Activity types should be creative and context-appropriate; do not limit yourself to a fixed list.', 'For each block, choose an availability mode: online (available to chat), do-not-disturb (busy but reachable for urgent matters), or offline (completely unavailable).', 'Return blocks covering the requested time range with startTime and endTime in HH:MM 24-hour format.', ].join('\n'); } export function blocksToDailySchedule(input: { persona: MemorySpace; targetDay: DateTimeInput; message: string; blocks: ScheduleBlock[]; }): ScheduleEntry[] { const target = startOfUtcDay(input.targetDay); return input.blocks.map((block) => { const [startHour, startMinute] = block.startTime.split(':').map(Number) as [number, number]; const [endHour, endMinute] = block.endTime.split(':').map(Number) as [number, number]; const start = new Date(target.getTime() + ((startHour * 60 + startMinute) * 60 * 1000)); const end = new Date(target.getTime() + ((endHour * 60 + endMinute) * 60 * 1000)); return { id: crypto.randomUUID(), spaceId: input.persona.id, startAt: start.toISOString(), endAt: end.toISOString(), activity: block.activity, title: block.title, description: block.description ?? `Realistic ${block.title.toLowerCase()} block for ${input.persona.displayName}.`, granularity: 'ten-minute', sourceMessage: input.message, metadata: { boxbrainType: 'schedule-entry', availabilityMode: block.availabilityMode, targetDate: target.toISOString().slice(0, 10), }, }; }); } export function blocksToMonthlySchedule(input: { persona: MemorySpace; fromDay: DateTimeInput; message: string; blocks: ScheduleBlock[]; }): ScheduleEntry[] { const start = startOfUtcDay(input.fromDay); return input.blocks.map((block, day) => { const dayStart = new Date(start.getTime() + day * DAY_MS); const [startHour, startMinute] = block.startTime.split(':').map(Number) as [number, number]; const [endHour, endMinute] = block.endTime.split(':').map(Number) as [number, number]; const entryStart = new Date(dayStart.getTime() + ((startHour * 60 + startMinute) * 60 * 1000)); const entryEnd = new Date(dayStart.getTime() + ((endHour * 60 + endMinute) * 60 * 1000)); return { id: crypto.randomUUID(), spaceId: input.persona.id, startAt: entryStart.toISOString(), endAt: entryEnd.toISOString(), activity: block.activity, title: block.title, description: block.description ?? `Daily outline for ${input.persona.displayName}.`, granularity: 'day', sourceMessage: input.message, metadata: { boxbrainType: 'schedule-entry', availabilityMode: block.availabilityMode, targetDate: dayStart.toISOString().slice(0, 10), }, }; }); } export function availabilityModeForEntry(entry: ScheduleEntry): AvailabilityMode { const mode = entry.metadata['availabilityMode']; if (mode === 'online' || mode === 'do-not-disturb' || mode === 'offline') return mode; return 'online'; } export function buildAvailabilitySnapshot(input: { now: DateTimeInput; generatedAt?: DateTimeInput; entries: ScheduleEntry[]; }): ScheduledAvailabilitySnapshot { const windowStart = startOfUtcDay(input.now); const windowEnd = new Date(windowStart.getTime() + 2 * DAY_MS); const sorted = input.entries .filter((entry) => entry.startAt < windowEnd.toISOString() && entry.endAt > windowStart.toISOString()) .sort((a, b) => a.startAt.localeCompare(b.startAt)); const ranges: AvailabilityRange[] = []; for (const entry of sorted) { const mode = availabilityModeForEntry(entry); const previous = ranges.at(-1); if (previous && previous.mode === mode && previous.endAt === entry.startAt) { previous.endAt = entry.endAt; previous.sourceScheduleIds.push(entry.id); continue; } ranges.push({ startAt: entry.startAt, endAt: entry.endAt, mode, sourceScheduleIds: [entry.id], reason: entry.title, }); } return { generatedAt: toIso(input.generatedAt ?? input.now), windowStartAt: windowStart.toISOString(), windowEndAt: windowEnd.toISOString(), ranges, }; } export function dateKeysAround(input: DateTimeInput): string[] { const today = startOfUtcDay(input); return [-1, 0, 1].map((offset) => new Date(today.getTime() + offset * DAY_MS).toISOString().slice(0, 10)); }