import { WizardStep } from '@/lib/types/WizardStep'; import { WizardErrors } from '@/lib/types/WizardErrors'; import { CreateLeagueInputDTO } from '@/lib/types/CreateLeagueInputDTO'; import { LeagueWizardValidationMessages } from '@/lib/display-objects/LeagueWizardValidationMessages'; import { ScoringPresetApplier } from '@/lib/utilities/ScoringPresetApplier'; type LeagueWizardFormData = { leagueId: string | undefined; basics: { name: string; description?: string; visibility: 'public' | 'private' | 'unlisted'; gameId: string; }; structure: { mode: 'solo' | 'fixedTeams'; maxDrivers?: number; maxTeams?: number; driversPerTeam?: number; }; championships: { enableDriverChampionship: boolean; enableTeamChampionship: boolean; enableNationsChampionship: boolean; enableTrophyChampionship: boolean; }; scoring: { patternId?: string; customScoringEnabled?: boolean; }; dropPolicy: { strategy: 'none' | 'bestNResults' | 'dropWorstN'; n?: number; }; timings: { practiceMinutes?: number; qualifyingMinutes?: number; sprintRaceMinutes?: number; mainRaceMinutes?: number; sessionCount?: number; roundsPlanned?: number; raceDayOfWeek?: number; raceTimeUtc?: string; }; stewarding: { decisionMode: 'owner_only' | 'admin_vote' | 'steward_panel'; requiredVotes?: number; requireDefense: boolean; defenseTimeLimit: number; voteTimeLimit: number; protestDeadlineHours: number; stewardingClosesHours: number; notifyAccusedOnProtest: boolean; notifyOnVoteRequired: boolean; }; }; export class LeagueWizardCommandModel { leagueId: string | undefined; basics: LeagueWizardFormData['basics']; structure: LeagueWizardFormData['structure']; championships: LeagueWizardFormData['championships']; scoring: LeagueWizardFormData['scoring']; dropPolicy: LeagueWizardFormData['dropPolicy']; timings: LeagueWizardFormData['timings']; stewarding: LeagueWizardFormData['stewarding']; constructor(initial: Partial = {}) { this.leagueId = initial.leagueId; this.basics = { name: '', description: '', visibility: 'public', gameId: '', ...initial.basics, }; this.structure = { mode: 'solo', ...initial.structure, }; this.championships = { enableDriverChampionship: true, enableTeamChampionship: false, enableNationsChampionship: false, enableTrophyChampionship: false, ...initial.championships, }; this.scoring = { ...initial.scoring, }; this.dropPolicy = { strategy: 'none', ...initial.dropPolicy, }; this.timings = { ...initial.timings, }; this.stewarding = { decisionMode: 'owner_only', requireDefense: false, defenseTimeLimit: 24, voteTimeLimit: 48, protestDeadlineHours: 24, stewardingClosesHours: 168, notifyAccusedOnProtest: true, notifyOnVoteRequired: true, ...initial.stewarding, }; } validateStep(step: WizardStep): WizardErrors { const errors: WizardErrors = {}; // Step 1: Basics if (step === 1) { const basicsErrors: NonNullable = {}; if (!this.basics.name || this.basics.name.trim().length === 0) { basicsErrors.name = LeagueWizardValidationMessages.LEAGUE_NAME_REQUIRED; } else if (this.basics.name.length < 3) { basicsErrors.name = LeagueWizardValidationMessages.LEAGUE_NAME_TOO_SHORT; } else if (this.basics.name.length > 100) { basicsErrors.name = LeagueWizardValidationMessages.LEAGUE_NAME_TOO_LONG; } if (this.basics.description && this.basics.description.length > 500) { basicsErrors.description = LeagueWizardValidationMessages.DESCRIPTION_TOO_LONG; } if (Object.keys(basicsErrors).length > 0) { errors.basics = basicsErrors; } } // Step 2: Visibility if (step === 2) { const basicsErrors: NonNullable = {}; if (!this.basics.visibility) { basicsErrors.visibility = LeagueWizardValidationMessages.VISIBILITY_REQUIRED; } if (Object.keys(basicsErrors).length > 0) { errors.basics = basicsErrors; } } // Step 3: Structure if (step === 3) { const structureErrors: NonNullable = {}; if (this.structure.mode === 'solo') { if (!this.structure.maxDrivers || this.structure.maxDrivers <= 0) { structureErrors.maxDrivers = LeagueWizardValidationMessages.MAX_DRIVERS_INVALID_SOLO; } else if (this.structure.maxDrivers > 100) { structureErrors.maxDrivers = LeagueWizardValidationMessages.MAX_DRIVERS_TOO_HIGH; } } else if (this.structure.mode === 'fixedTeams') { if (!this.structure.maxTeams || this.structure.maxTeams <= 0) { structureErrors.maxTeams = LeagueWizardValidationMessages.MAX_TEAMS_INVALID_TEAM; } if (!this.structure.driversPerTeam || this.structure.driversPerTeam <= 0) { structureErrors.driversPerTeam = LeagueWizardValidationMessages.DRIVERS_PER_TEAM_INVALID; } } if (Object.keys(structureErrors).length > 0) { errors.structure = structureErrors; } } // Step 4: Timings if (step === 4) { const timingsErrors: NonNullable = {}; if (!this.timings.qualifyingMinutes || this.timings.qualifyingMinutes <= 0) { timingsErrors.qualifyingMinutes = LeagueWizardValidationMessages.QUALIFYING_DURATION_INVALID; } if (!this.timings.mainRaceMinutes || this.timings.mainRaceMinutes <= 0) { timingsErrors.mainRaceMinutes = LeagueWizardValidationMessages.MAIN_RACE_DURATION_INVALID; } if (Object.keys(timingsErrors).length > 0) { errors.timings = timingsErrors; } } // Step 5: Scoring if (step === 5) { const scoringErrors: NonNullable = {}; if (!this.scoring.patternId && !this.scoring.customScoringEnabled) { scoringErrors.patternId = LeagueWizardValidationMessages.SCORING_PRESET_OR_CUSTOM_REQUIRED; } if (Object.keys(scoringErrors).length > 0) { errors.scoring = scoringErrors; } } return errors; } validateAll(): WizardErrors { const aggregate: WizardErrors = {}; const merge = (next: WizardErrors) => { if (next.basics) { aggregate.basics = { ...aggregate.basics, ...next.basics }; } if (next.structure) { aggregate.structure = { ...aggregate.structure, ...next.structure }; } if (next.timings) { aggregate.timings = { ...aggregate.timings, ...next.timings }; } if (next.scoring) { aggregate.scoring = { ...aggregate.scoring, ...next.scoring }; } if (next.submit) { aggregate.submit = next.submit; } }; merge(this.validateStep(1)); merge(this.validateStep(2)); merge(this.validateStep(3)); merge(this.validateStep(4)); merge(this.validateStep(5)); return aggregate; } static hasWizardErrors(errors: WizardErrors): boolean { return Object.keys(errors).some((key) => { const value = errors[key as keyof WizardErrors]; if (!value) return false; if (typeof value === 'string') return true; return Object.keys(value).length > 0; }); } applyScoringPreset(patternId: string): void { this.scoring = { ...this.scoring, patternId, customScoringEnabled: false, }; this.timings = ScoringPresetApplier.applyToTimings(patternId, this.timings); } toCreateLeagueCommand(ownerId: string): CreateLeagueInputDTO { let maxMembers: number; if (this.structure.mode === 'solo') { maxMembers = this.structure.maxDrivers ?? 0; } else { const teams = this.structure.maxTeams ?? 0; const perTeam = this.structure.driversPerTeam ?? 0; maxMembers = teams * perTeam; } return { name: this.basics.name.trim(), description: this.basics.description?.trim() ?? '', isPublic: this.basics.visibility === 'public', maxMembers, ownerId, }; } // Static methods for backward compatibility with component usage static validateLeagueWizardStep(form: LeagueWizardFormData, step: WizardStep): WizardErrors { const instance = new LeagueWizardCommandModel(form); return instance.validateStep(step); } static validateAllLeagueWizardSteps(form: LeagueWizardFormData): WizardErrors { const instance = new LeagueWizardCommandModel(form); return instance.validateAll(); } static applyScoringPresetToConfig(form: LeagueWizardFormData, patternId: string): LeagueWizardFormData { const instance = new LeagueWizardCommandModel(form); instance.applyScoringPreset(patternId); return { leagueId: instance.leagueId, basics: instance.basics, structure: instance.structure, championships: instance.championships, scoring: instance.scoring, dropPolicy: instance.dropPolicy, timings: instance.timings, stewarding: instance.stewarding, }; } }