import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import { Result as RaceResult } from '../../domain/entities/result/Result';
import { Standing } from '../../domain/entities/Standing';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';

export interface CompleteRaceInput {
  raceId: string;
}

export type CompleteRaceResult = {
  raceId: string;
  registeredDriverIds: string[];
};

export type CompleteRaceErrorCode = 'RACE_NOT_FOUND' | 'NO_REGISTERED_DRIVERS' | 'REPOSITORY_ERROR';

/**
 * Use Case: CompleteRaceUseCase
 *
 * Encapsulates the workflow for completing a race:
 * - loads the race by id
 * - returns error if the race does not exist
 * - delegates completion rules to the Race domain entity
 * - automatically generates realistic results for registered drivers
 * - updates league standings
 * - persists all changes via repositories.
 */
interface DriverRatingInput {
  driverId: string;
}

interface DriverRatingOutput {
  rating: number | null;
  ratingChange: number | null;
}

export class CompleteRaceUseCase {
  constructor(
    private readonly raceRepository: IRaceRepository,
    private readonly raceRegistrationRepository: IRaceRegistrationRepository,
    private readonly resultRepository: IResultRepository,
    private readonly standingRepository: IStandingRepository,
    private readonly getDriverRating: (input: DriverRatingInput) => Promise<DriverRatingOutput>,
    private readonly output: UseCaseOutputPort<CompleteRaceResult>,
  ) {}

  async execute(command: CompleteRaceInput): Promise<
    Result<void, ApplicationErrorCode<CompleteRaceErrorCode | 'REPOSITORY_ERROR', { message: string }>>
  > {
    try {
      const { raceId } = command;

      const race = await this.raceRepository.findById(raceId);
      if (!race) {
        return Result.err<void, ApplicationErrorCode<CompleteRaceErrorCode, { message: string }>>({
          code: 'RACE_NOT_FOUND',
          details: { message: 'Race not found' },
        });
      }

      // Get registered drivers for this race
      const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId);
      if (registeredDriverIds.length === 0) {
        return Result.err<void, ApplicationErrorCode<CompleteRaceErrorCode, { message: string }>>({
          code: 'NO_REGISTERED_DRIVERS',
          details: { message: 'No registered drivers for this race' },
        });
      }

      // Get driver ratings using injected provider
      const ratingPromises = registeredDriverIds.map((driverId) =>
        this.getDriverRating({ driverId }),
      );

      const ratingResults = await Promise.all(ratingPromises);
      const driverRatings = new Map<string, number>();

      registeredDriverIds.forEach((driverId, index) => {
        const rating = ratingResults[index]?.rating ?? null;
        if (rating !== null) {
          driverRatings.set(driverId, rating);
        }
      });

      // Generate realistic race results
      const results = this.generateRaceResults(raceId, registeredDriverIds, driverRatings);

      // Save results
      for (const result of results) {
        await this.resultRepository.create(result);
      }

      // Update standings
      await this.updateStandings(race.leagueId, results);

      // Complete the race
      const completedRace = race.complete();
      await this.raceRepository.update(completedRace);

      this.output.present({ raceId, registeredDriverIds });

      return Result.ok(undefined);
    } catch (error) {
      return Result.err({
        code: 'REPOSITORY_ERROR',
        details: {
          message: error instanceof Error ? error.message : 'Unknown error',
        },
      });
    }
  }

  private generateRaceResults(
    raceId: string,
    driverIds: string[],
    driverRatings: Map<string, number>,
  ): RaceResult[] {
    // Create driver performance data
    const driverPerformances = driverIds.map(driverId => ({
      driverId,
      rating: driverRatings.get(driverId) ?? 1500, // Default rating
      randomFactor: Math.random() - 0.5, // -0.5 to +0.5 randomization
    }));

    // Sort by performance (rating + randomization)
    driverPerformances.sort((a, b) => {
      const perfA = a.rating + (a.randomFactor * 200); // ±100 rating points randomization
      const perfB = b.rating + (b.randomFactor * 200);
      return perfB - perfA; // Higher performance first
    });

    // Generate qualifying results for start positions (similar but different from race results)
    const qualiPerformances = driverPerformances.map(p => ({
      ...p,
      randomFactor: Math.random() - 0.5, // New randomization for quali
    }));
    qualiPerformances.sort((a, b) => {
      const perfA = a.rating + (a.randomFactor * 150);
      const perfB = b.rating + (b.randomFactor * 150);
      return perfB - perfA;
    });

    // Generate results
    const results: RaceResult[] = [];
    for (let i = 0; i < driverPerformances.length; i++) {
      const { driverId } = driverPerformances[i]!;
      const position = i + 1;
      const startPosition = qualiPerformances.findIndex(p => p.driverId === driverId) + 1;

      // Generate realistic lap times (90-120 seconds for a lap)
      const baseLapTime = 90000 + Math.random() * 30000;
      const positionBonus = (position - 1) * 500; // Winners are faster
      const fastestLap = Math.round(baseLapTime + positionBonus + Math.random() * 5000);

      // Generate incidents (0-3, higher for lower positions)
      const incidentProbability = Math.min(0.8, position / driverPerformances.length);
      const incidents = Math.random() < incidentProbability ? Math.floor(Math.random() * 3) + 1 : 0;

      results.push(
        RaceResult.create({
          id: `${raceId}-${driverId}`,
          raceId,
          driverId,
          position,
          startPosition,
          fastestLap,
          incidents,
        }),
      );
    }

    return results;
  }

  private async updateStandings(leagueId: string, results: RaceResult[]): Promise<void> {
    // Group results by driver
    const resultsByDriver = new Map<string, RaceResult[]>();
    for (const result of results) {
      const driverIdStr = result.driverId.toString();
      const existing = resultsByDriver.get(driverIdStr) || [];
      existing.push(result);
      resultsByDriver.set(driverIdStr, existing);
    }

    // Update or create standings for each driver
    for (const [driverId, driverResults] of resultsByDriver) {
      let standing = await this.standingRepository.findByDriverIdAndLeagueId(driverId, leagueId);

      if (!standing) {
        standing = Standing.create({
          leagueId,
          driverId,
        });
      }

      // Add all results for this driver (should be just one for this race)
      for (const result of driverResults) {
        standing = standing.addRaceResult(result.position.toNumber(), {
          1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1
        });
      }

      await this.standingRepository.save(standing);
    }
  }
}