import type { Logger } from '@core/shared/domain/Logger'; import { Result } from '@core/shared/domain/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { SeasonScheduleGenerator } from '../../domain/services/SeasonScheduleGenerator'; import { ALL_WEEKDAYS, type Weekday } from '../../domain/types/Weekday'; import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone'; import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern'; import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay'; import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy'; import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule'; import { WeekdaySet } from '../../domain/value-objects/WeekdaySet'; export type SeasonScheduleConfig = { seasonStartDate: string; recurrenceStrategy: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday'; weekdays?: string[]; raceStartTime: string; timezoneId: string; plannedRounds: number; intervalWeeks?: number; monthlyOrdinal?: 1 | 2 | 3 | 4; monthlyWeekday?: string; }; function toWeekdaySet(values: string[] | undefined): WeekdaySet { const weekdays = (values ?? []).filter((v): v is Weekday => ALL_WEEKDAYS.includes(v as Weekday), ); return WeekdaySet.fromArray(weekdays.length > 0 ? weekdays : ['Mon']); } function scheduleConfigToSeasonSchedule(dto: SeasonScheduleConfig): SeasonSchedule { const startDate = new Date(dto.seasonStartDate); const timeOfDay = RaceTimeOfDay.fromString(dto.raceStartTime); const timezone = LeagueTimezone.create(dto.timezoneId); const recurrence = (() => { switch (dto.recurrenceStrategy) { case 'everyNWeeks': return RecurrenceStrategyFactory.everyNWeeks( dto.intervalWeeks ?? 2, toWeekdaySet(dto.weekdays), ); case 'monthlyNthWeekday': { const pattern = MonthlyRecurrencePattern.create( dto.monthlyOrdinal ?? 1, ((dto.monthlyWeekday ?? 'Mon') as Weekday), ); return RecurrenceStrategyFactory.monthlyNthWeekday(pattern); } case 'weekly': default: return RecurrenceStrategyFactory.weekly(toWeekdaySet(dto.weekdays)); } })(); return new SeasonSchedule({ startDate, timeOfDay, timezone, recurrence, plannedRounds: dto.plannedRounds, }); } export type PreviewLeagueScheduleSeasonConfig = SeasonScheduleConfig; export type PreviewLeagueScheduleInput = { schedule: PreviewLeagueScheduleSeasonConfig; maxRounds?: number; }; export interface PreviewLeagueScheduleRound { roundNumber: number; scheduledAt: string; timezoneId: string; } export interface PreviewLeagueScheduleResult { rounds: PreviewLeagueScheduleRound[]; summary: string; } export type PreviewLeagueScheduleErrorCode = | 'INVALID_SCHEDULE' | 'REPOSITORY_ERROR'; export class PreviewLeagueScheduleUseCase { constructor(private readonly scheduleGenerator: Pick< typeof SeasonScheduleGenerator, 'generateSlotsUpTo' > = SeasonScheduleGenerator, private readonly logger: Logger) {} async execute( params: PreviewLeagueScheduleInput, ): Promise< Result< PreviewLeagueScheduleResult, ApplicationErrorCode > > { this.logger.debug('Previewing league schedule', { params }); try { let seasonSchedule: SeasonSchedule; try { seasonSchedule = scheduleConfigToSeasonSchedule(params.schedule); } catch (error) { this.logger.warn('Invalid schedule data provided', { schedule: params.schedule, error: error instanceof Error ? error.message : 'Unknown error', }); return Result.err({ code: 'INVALID_SCHEDULE', details: { message: 'Invalid schedule data' }, }); } const maxRounds = params.maxRounds && params.maxRounds > 0 ? Math.min(params.maxRounds, seasonSchedule.plannedRounds) : seasonSchedule.plannedRounds; const slots = this.scheduleGenerator.generateSlotsUpTo( seasonSchedule, maxRounds, ); const rounds: PreviewLeagueScheduleRound[] = slots.map(slot => ({ roundNumber: slot.roundNumber, scheduledAt: slot.scheduledAt.toISOString(), timezoneId: slot.timezone.id, })); const summary = this.buildSummary(params.schedule, rounds); const result: PreviewLeagueScheduleResult = { rounds, summary, }; this.logger.info('Successfully generated league schedule preview', { roundCount: rounds.length, }); return Result.ok(result); } catch (error) { this.logger.error( 'Failed to preview league schedule due to an unexpected error', error instanceof Error ? error : new Error('Unknown error'), ); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Failed to preview league schedule', }, }); } } private buildSummary( schedule: PreviewLeagueScheduleSeasonConfig, rounds: Array<{ roundNumber: number; scheduledAt: string; timezoneId: string }>, ): string { if (rounds.length === 0) { return 'No rounds scheduled.'; } const firstRound = rounds[0]!; const lastRound = rounds[rounds.length - 1]!; const first = new Date(firstRound.scheduledAt); const last = new Date(lastRound.scheduledAt); const firstDate = first.toISOString().slice(0, 10); const lastDate = last.toISOString().slice(0, 10); const timePart = schedule.raceStartTime; const tz = schedule.timezoneId; let recurrenceDescription: string; if (schedule.recurrenceStrategy === 'weekly') { const days = (schedule.weekdays ?? []).join(', '); recurrenceDescription = `Every ${days}`; } else if (schedule.recurrenceStrategy === 'everyNWeeks') { const interval = schedule.intervalWeeks ?? 1; const days = (schedule.weekdays ?? []).join(', '); recurrenceDescription = `Every ${interval} week(s) on ${days}`; } else if (schedule.recurrenceStrategy === 'monthlyNthWeekday') { const ordinalLabel = this.ordinalToLabel(schedule.monthlyOrdinal ?? 1); const weekday = schedule.monthlyWeekday ?? 'Mon'; recurrenceDescription = `Every ${ordinalLabel} ${weekday}`; } else { recurrenceDescription = 'Custom recurrence'; } return `${recurrenceDescription} at ${timePart} ${tz}, starting ${firstDate} — ${rounds.length} rounds from ${firstDate} to ${lastDate}.`; } private ordinalToLabel(ordinal: 1 | 2 | 3 | 4): string { switch (ordinal) { case 1: return '1st'; case 2: return '2nd'; case 3: return '3rd'; case 4: return '4th'; default: return `${ordinal}th`; } } }