import { SeasonScheduleGenerator } from '../../domain/services/SeasonScheduleGenerator'; import type { LeagueScheduleDTO } from '../dto/LeagueScheduleDTO'; import { scheduleDTOToSeasonSchedule } from '../dto/LeagueScheduleDTO'; import type { LeagueSchedulePreviewOutputPort } from '../ports/output/LeagueSchedulePreviewOutputPort'; import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { Logger } from '@core/shared/application'; import type { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule'; interface PreviewLeagueScheduleQueryParams { schedule: LeagueScheduleDTO; maxRounds?: number; } type PreviewLeagueScheduleErrorCode = 'INVALID_SCHEDULE'; type PreviewLeagueScheduleApplicationError = ApplicationErrorCode; export class PreviewLeagueScheduleUseCase implements AsyncUseCase { constructor( private readonly scheduleGenerator: typeof SeasonScheduleGenerator = SeasonScheduleGenerator, private readonly logger: Logger, ) {} async execute(params: PreviewLeagueScheduleQueryParams): Promise> { this.logger.debug('Previewing league schedule', { params }); let seasonSchedule: SeasonSchedule; try { seasonSchedule = scheduleDTOToSeasonSchedule(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 = slots.map((slot) => ({ roundNumber: slot.roundNumber, scheduledAt: slot.scheduledAt.toISOString(), timezoneId: slot.timezone.getId(), })); const summary = this.buildSummary(params.schedule, rounds); const result: LeagueSchedulePreviewOutputPort = { rounds, summary, }; this.logger.info('Successfully generated league schedule preview', { roundCount: rounds.length }); return Result.ok(result); } private buildSummary( schedule: LeagueScheduleDTO, 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`; } } }