import { LeaguesApiClient } from "@/lib/api/leagues/LeaguesApiClient"; import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient"; import { SponsorsApiClient } from "@/lib/api/sponsors/SponsorsApiClient"; import { RacesApiClient } from "@/lib/api/races/RacesApiClient"; import { CreateLeagueInputDTO } from "@/lib/types/generated/CreateLeagueInputDTO"; import { CreateLeagueOutputDTO } from "@/lib/types/generated/CreateLeagueOutputDTO"; import { LeagueWithCapacityDTO } from "@/lib/types/generated/LeagueWithCapacityDTO"; import { CreateLeagueViewModel } from "@/lib/view-models/CreateLeagueViewModel"; import { LeagueMembershipsViewModel } from "@/lib/view-models/LeagueMembershipsViewModel"; import { LeagueScheduleViewModel } from "@/lib/view-models/LeagueScheduleViewModel"; import { LeagueStandingsViewModel } from "@/lib/view-models/LeagueStandingsViewModel"; import { LeagueStatsViewModel } from "@/lib/view-models/LeagueStatsViewModel"; import { LeagueSummaryViewModel } from "@/lib/view-models/LeagueSummaryViewModel"; import { RemoveMemberViewModel } from "@/lib/view-models/RemoveMemberViewModel"; import { LeaguePageDetailViewModel } from "@/lib/view-models/LeaguePageDetailViewModel"; import { LeagueDetailPageViewModel, SponsorInfo } from "@/lib/view-models/LeagueDetailPageViewModel"; import { RaceViewModel } from "@/lib/view-models/RaceViewModel"; import { SubmitBlocker, ThrottleBlocker } from "@/lib/blockers"; import { RaceDTO } from "@/lib/types/generated/RaceDTO"; import { LeagueStatsDTO } from "@/lib/types/generated/LeagueStatsDTO"; import { LeagueScoringConfigDTO } from "@/lib/types/generated/LeagueScoringConfigDTO"; import type { LeagueMembership } from "@/lib/types/LeagueMembership"; import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO'; /** * League Service * * Orchestrates league operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class LeagueService { private readonly submitBlocker = new SubmitBlocker(); private readonly throttle = new ThrottleBlocker(500); constructor( private readonly apiClient: LeaguesApiClient, private readonly driversApiClient?: DriversApiClient, private readonly sponsorsApiClient?: SponsorsApiClient, private readonly racesApiClient?: RacesApiClient ) {} /** * Get all leagues with view model transformation */ async getAllLeagues(): Promise { const dto = await this.apiClient.getAllWithCapacity(); return dto.leagues.map((league: LeagueWithCapacityDTO) => ({ id: league.id, name: league.name, description: (league as any).description ?? '', ownerId: (league as any).ownerId ?? '', createdAt: (league as any).createdAt ?? '', maxDrivers: (league as any).settings?.maxDrivers ?? 0, usedDriverSlots: (league as any).usedSlots ?? 0, structureSummary: 'TBD', timingSummary: 'TBD' })); } /** * Get league standings with view model transformation */ async getLeagueStandings(leagueId: string, currentUserId: string): Promise { // Core standings (positions, points, driverIds) const dto = await this.apiClient.getStandings(leagueId); const standings = ((dto as any)?.standings ?? []) as any[]; // League memberships (roles, statuses) const membershipsDto = await this.apiClient.getMemberships(leagueId); const membershipEntries = ((membershipsDto as any)?.members ?? (membershipsDto as any)?.memberships ?? []) as any[]; const memberships: LeagueMembership[] = membershipEntries.map((m) => ({ driverId: m.driverId, leagueId, role: (m.role as LeagueMembership['role']) ?? 'member', joinedAt: m.joinedAt, status: 'active', })); // Resolve unique drivers that appear in standings const driverIds: string[] = Array.from(new Set(standings.map((entry: any) => entry.driverId))); const driverDtos = this.driversApiClient ? await Promise.all(driverIds.map((id: string) => this.driversApiClient!.getDriver(id))) : []; const drivers = driverDtos.filter((d): d is NonNullable => d !== null); const dtoWithExtras = { standings, drivers, memberships }; return new LeagueStandingsViewModel(dtoWithExtras, currentUserId); } /** * Get league statistics */ async getLeagueStats(): Promise { const dto = await this.apiClient.getTotal(); return new LeagueStatsViewModel(dto); } /** * Get league schedule */ async getLeagueSchedule(leagueId: string): Promise { const dto = await this.apiClient.getSchedule(leagueId); return new LeagueScheduleViewModel(dto); } /** * Get seasons for a league */ async getLeagueSeasons(leagueId: string): Promise { return this.apiClient.getSeasons(leagueId); } /** * Get league memberships */ async getLeagueMemberships(leagueId: string, currentUserId: string): Promise { const dto = await this.apiClient.getMemberships(leagueId); return new LeagueMembershipsViewModel(dto, currentUserId); } /** * Create a new league */ async createLeague(input: CreateLeagueInputDTO): Promise { if (!this.submitBlocker.canExecute() || !this.throttle.canExecute()) { return { success: false, leagueId: '' } as CreateLeagueOutputDTO; } this.submitBlocker.block(); this.throttle.block(); try { return await this.apiClient.create(input); } finally { this.submitBlocker.release(); } } /** * Remove a member from league */ async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise { const dto = await this.apiClient.removeMember(leagueId, performerDriverId, targetDriverId); return new RemoveMemberViewModel(dto); } /** * Update a member's role in league */ async updateMemberRole(leagueId: string, performerDriverId: string, targetDriverId: string, newRole: string): Promise<{ success: boolean }> { return this.apiClient.updateMemberRole(leagueId, performerDriverId, targetDriverId, newRole); } /** * Get league detail with owner, membership, and sponsor info */ async getLeagueDetail(leagueId: string, currentDriverId: string): Promise { if (!this.driversApiClient) return null; // For now, assume league data comes from getAllWithCapacity or a new endpoint // Since API may not have detailed league, we'll mock or assume // In real implementation, add getLeagueDetail to API const allLeagues = await this.apiClient.getAllWithCapacity(); const leagueDto = allLeagues.leagues.find((l: any) => l.id === leagueId); if (!leagueDto) return null; // LeagueWithCapacityDTO already carries core fields; fall back to placeholder description/owner when not provided const league = { id: leagueDto.id, name: leagueDto.name, description: (leagueDto as any).description ?? 'Description not available', ownerId: (leagueDto as any).ownerId ?? 'owner-id', }; // Get owner const owner = await this.driversApiClient.getDriver(league.ownerId); const ownerName = owner ? (owner as any).name : `${league.ownerId.slice(0, 8)}...`; // Get membership const membershipsDto = await this.apiClient.getMemberships(leagueId); const membership = membershipsDto.members.find((m: any) => m.driverId === currentDriverId); const isAdmin = membership ? ['admin', 'owner'].includes((membership as any).role) : false; // Get main sponsor let mainSponsor = null; if (this.sponsorsApiClient) { try { const seasons = await this.apiClient.getSeasons(leagueId); const activeSeason = seasons.find((s) => s.status === 'active') ?? seasons[0]; if (activeSeason) { const sponsorshipsDto = await this.apiClient.getSeasonSponsorships(activeSeason.seasonId); const mainSponsorship = sponsorshipsDto.sponsorships.find((s: any) => s.tier === 'main' && s.status === 'active'); if (mainSponsorship) { const sponsorId = (mainSponsorship as any).sponsorId ?? (mainSponsorship as any).sponsor?.id; const sponsorResult = await this.sponsorsApiClient.getSponsor(sponsorId); const sponsor = (sponsorResult as any)?.sponsor ?? null; if (sponsor) { mainSponsor = { name: sponsor.name, logoUrl: sponsor.logoUrl ?? '', websiteUrl: sponsor.websiteUrl ?? '', }; } } } } catch (error) { console.warn('Failed to load main sponsor:', error); } } return new LeaguePageDetailViewModel({ league: { id: league.id, name: league.name, game: 'iRacing', tier: 'standard', season: 'Season 1', description: league.description, drivers: 0, races: 0, completedRaces: 0, totalImpressions: 0, avgViewsPerRace: 0, engagement: 0, rating: 0, seasonStatus: 'active', seasonDates: { start: new Date().toISOString(), end: new Date().toISOString() }, sponsorSlots: { main: { available: true, price: 800, benefits: [] }, secondary: { available: 2, total: 2, price: 250, benefits: [] } } }, drivers: [], races: [] }); } /** * Get comprehensive league detail page data */ async getLeagueDetailPageData(leagueId: string): Promise { if (!this.driversApiClient || !this.sponsorsApiClient) return null; try { // Get league basic info const allLeagues = await this.apiClient.getAllWithCapacity(); const league = allLeagues.leagues.find((l: any) => l.id === leagueId); if (!league) return null; // Get owner const owner = await this.driversApiClient.getDriver((league as any).ownerId); // League scoring configuration is not exposed separately yet; use null to indicate "not configured" in the UI const scoringConfig: LeagueScoringConfigDTO | null = null; // Drivers list is limited to those present in memberships until a dedicated league-drivers endpoint exists const memberships = await this.apiClient.getMemberships(leagueId); const driverIds = memberships.members.map((m: any) => m.driverId); const driverDtos = await Promise.all(driverIds.map((id: string) => this.driversApiClient!.getDriver(id))); const drivers = driverDtos.filter((d: any): d is NonNullable => d !== null); // Get all races for this league via the leagues API helper const leagueRaces = await this.apiClient.getRaces(leagueId); const allRaces = leagueRaces.races.map(r => new RaceViewModel(r as RaceDTO)); // League stats endpoint currently returns global league statistics rather than per-league values const leagueStats: LeagueStatsDTO = { totalMembers: league.usedSlots, totalRaces: allRaces.length, averageRating: 0, }; // Get sponsors const sponsors = await this.getLeagueSponsors(leagueId); return new LeagueDetailPageViewModel( league, owner, scoringConfig, drivers, memberships, allRaces, leagueStats, sponsors ); } catch (error) { console.error('Failed to load league detail page data:', error); return null; } } /** * Get sponsors for a league */ private async getLeagueSponsors(leagueId: string): Promise { if (!this.sponsorsApiClient) return []; try { const seasons = await this.apiClient.getSeasons(leagueId); const activeSeason = seasons.find((s) => s.status === 'active') ?? seasons[0]; if (!activeSeason) return []; const sponsorships = await this.apiClient.getSeasonSponsorships(activeSeason.seasonId); const activeSponsorships = sponsorships.sponsorships.filter((s: any) => s.status === 'active'); const sponsorInfos: SponsorInfo[] = []; for (const sponsorship of activeSponsorships) { const sponsorResult = await this.sponsorsApiClient.getSponsor((sponsorship as any).sponsorId ?? (sponsorship as any).sponsor?.id); const sponsor = (sponsorResult as any)?.sponsor ?? null; if (sponsor) { // Tagline is not supplied by the sponsor API in this build; callers may derive one from marketing content if needed sponsorInfos.push({ id: sponsor.id, name: sponsor.name, logoUrl: sponsor.logoUrl ?? '', websiteUrl: sponsor.websiteUrl ?? '', tier: ((sponsorship as any).tier as 'main' | 'secondary') ?? 'secondary', tagline: '', }); } } // Sort: main sponsors first, then secondary sponsorInfos.sort((a, b) => { if (a.tier === 'main' && b.tier !== 'main') return -1; if (a.tier !== 'main' && b.tier === 'main') return 1; return 0; }); return sponsorInfos; } catch (error) { console.warn('Failed to load sponsors:', error); return []; } } /** * Get league scoring presets */ async getScoringPresets(): Promise { const result = await this.apiClient.getScoringPresets(); return result.presets; } }