import { HttpError } from '../../utils/http.js'; // DATETIME representa hora civil de America/Guayaquil (UTC-5 sin DST). Date en UTC se usa // solo para aritmética de calendario; no convierte estos valores a la zona del servidor. const parse = (s) => new Date(`${s.replace(' ', 'T')}Z`); const format = (d) => d.toISOString().slice(0, 19).replace('T', ' '); export const addMinutes = (s, n) => format(new Date(parse(s).getTime() + n * 60000)); export const durationMinutes = (a, b) => (parse(b) - parse(a)) / 60000; export const guayaquilNow = () => format(new Date(Date.now() - 5 * 3600000)); function* occurrenceStarts(start, rule, max = 365) { const first = parse(start); if (!rule) { yield start; return; } let emitted = 0; const accept = (d) => d >= first && d.getUTCFullYear() <= 9999 && (!rule.until || format(d).slice(0, 10) <= rule.until); if (rule.frequency === 'daily') { for (let n = 0; emitted < max && (!rule.count || emitted < rule.count); n++) { const d = new Date(first.getTime() + n * rule.interval * 86400000); if (!accept(d)) break; emitted++; yield format(d); } } else if (rule.frequency === 'weekly') { const monday = new Date(first.getTime() - ((first.getUTCDay() + 6) % 7) * 86400000); for (let block = 0; emitted < max && (!rule.count || emitted < rule.count); block++) { const week = new Date(monday.getTime() + block * rule.interval * 7 * 86400000); if (week.getUTCFullYear() > 9999 || (rule.until && format(week).slice(0, 10) > rule.until)) break; for (const weekday of rule.weekdays) { const d = new Date(week.getTime() + weekday * 86400000); if (d < first) continue; if (!accept(d)) return; emitted++; yield format(d); if (emitted >= max || (rule.count && emitted >= rule.count)) return; } } } else if (rule.frequency === 'monthly') { const time = [first.getUTCHours(), first.getUTCMinutes(), first.getUTCSeconds()]; for (let block = 0; emitted < max && (!rule.count || emitted < rule.count); block++) { const totalMonths = first.getUTCFullYear() * 12 + first.getUTCMonth() + block * rule.interval; const year = Math.floor(totalMonths / 12), month = totalMonths % 12; if (year > 9999 || (rule.until && `${year}-${String(month+1).padStart(2,'0')}-01` > rule.until)) break; const d = new Date(Date.UTC(year, month, first.getUTCDate(), ...time)); if (d.getUTCMonth() !== month) continue; // p. ej. no hay 31 de febrero. if (!accept(d)) break; emitted++; yield format(d); } } } export function validateRecurrence(raw, start) { if (!raw) return null; const { frequency, interval = 1, weekdays, until, count } = raw; if (!['daily', 'weekly', 'monthly'].includes(frequency) || !Number.isInteger(interval) || interval < 1 || interval > 365) throw new HttpError(400, 'Recurrencia invalida'); if ((!!until) === (!!count)) throw new HttpError(400, 'Indica fecha final o numero de ocurrencias'); if (count && (!Number.isInteger(count) || count < 1 || count > 365)) throw new HttpError(400, 'Maximo 365 ocurrencias'); if (until && (typeof until !== 'string' || !/^\d{4}-\d\d-\d\d$/.test(until) || Number.isNaN(Date.parse(`${until}T00:00:00Z`)) || new Date(`${until}T00:00:00Z`).toISOString().slice(0,10) !== until || until < start.slice(0, 10))) throw new HttpError(400, 'Fecha final de recurrencia invalida'); if (frequency === 'weekly' && (!Array.isArray(weekdays) || !weekdays.length || weekdays.some((v) => !Number.isInteger(v) || v < 0 || v > 6))) throw new HttpError(400, 'Dias semanales invalidos'); if (frequency === 'weekly' && !weekdays.includes((parse(start).getUTCDay() + 6) % 7)) throw new HttpError(400, 'El primer dia debe estar incluido'); const rule = { frequency, interval, weekdays: frequency === 'weekly' ? [...new Set(weekdays)].sort() : [], until: until || null, count: count || null }; const occurrences = [...occurrenceStarts(start, rule, 366)]; if (occurrences.length > 365) throw new HttpError(400, 'Maximo 365 ocurrencias'); if (count && occurrences.length < count) throw new HttpError(400, 'La serie supera el rango de fechas admitido'); return rule; } export function expand(meeting, from, to) { if (meeting.cancelled_at) return []; const rule = meeting.recurrence_json ? JSON.parse(meeting.recurrence_json) : null; const duration = durationMinutes(meeting.starts_at, meeting.ends_at); const exclusions = new Set(meeting.exceptions || []); const out = []; for (const start of occurrenceStarts(meeting.starts_at, rule)) { if (start >= to) break; const end = addMinutes(start, duration); if (end > from && !exclusions.has(start)) out.push({ ...meeting, occurrence_start: start, occurrence_end: end }); } return out; }