import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { Driver } from '../../domain/entities/Driver'; import type { Team } from '../../domain/entities/Team'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { IDriverStatsService } from '../../domain/services/IDriverStatsService'; import type { IRankingService } from '../../domain/services/IRankingService'; import { SkillLevelService, type SkillLevel } from '../../domain/services/SkillLevelService'; export type GetDriversLeaderboardInput = { leagueId?: string; seasonId?: string; }; export interface DriverLeaderboardItem { driver: Driver; team?: Team; rating: number; skillLevel: SkillLevel; racesCompleted: number; wins: number; podiums: number; isActive: boolean; rank: number; avatarUrl?: string; } export interface GetDriversLeaderboardResult { items: DriverLeaderboardItem[]; totalRaces: number; totalWins: number; activeCount: number; } export type GetDriversLeaderboardErrorCode = | 'LEAGUE_NOT_FOUND' | 'SEASON_NOT_FOUND' | 'REPOSITORY_ERROR'; /** * Use Case for retrieving driver leaderboard data. * Returns a Result containing the domain leaderboard model. */ export class GetDriversLeaderboardUseCase { constructor( private readonly driverRepository: IDriverRepository, private readonly rankingService: IRankingService, private readonly driverStatsService: IDriverStatsService, private readonly getDriverAvatar: (driverId: string) => Promise, private readonly logger: Logger, private readonly output: UseCaseOutputPort, ) {} async execute( input: GetDriversLeaderboardInput, ): Promise< Result< void, ApplicationErrorCode > > { this.logger.debug('Executing GetDriversLeaderboardUseCase', { input }); try { const drivers = await this.driverRepository.findAll(); const rankings = this.rankingService.getAllDriverRankings(); const avatarUrls: Record = {}; for (const driver of drivers) { avatarUrls[driver.id] = await this.getDriverAvatar(driver.id); } // TODO maps way too much data, should just create Domain Objects const items: DriverLeaderboardItem[] = drivers.map(driver => { const ranking = rankings.find(r => r.driverId === driver.id); const stats = this.driverStatsService.getDriverStats(driver.id); const rating = ranking?.rating ?? 0; const racesCompleted = stats?.totalRaces ?? 0; const skillLevel: SkillLevel = SkillLevelService.getSkillLevel(rating); const avatarUrl = avatarUrls[driver.id]; return { driver, rating, skillLevel, racesCompleted, wins: stats?.wins ?? 0, podiums: stats?.podiums ?? 0, isActive: racesCompleted > 0, rank: ranking?.overallRank ?? 0, ...(avatarUrl !== undefined ? { avatarUrl } : {}), }; }); const totalRaces = items.reduce((sum, d) => sum + d.racesCompleted, 0); const totalWins = items.reduce((sum, d) => sum + d.wins, 0); const activeCount = items.filter(d => d.isActive).length; const result: GetDriversLeaderboardResult = { items: items.sort((a, b) => b.rating - a.rating), totalRaces, totalWins, activeCount, }; this.logger.debug('Successfully computed drivers leaderboard'); this.output.present(result); return Result.ok(undefined); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); this.logger.error('Error executing GetDriversLeaderboardUseCase', err); return Result.err({ code: 'REPOSITORY_ERROR', details: { message: err.message ?? 'Unknown error occurred' }, }); } } }