import { SeasonSchedule } from '../value-objects/SeasonSchedule';
import { ScheduledRaceSlot } from '../value-objects/ScheduledRaceSlot';
import type { RecurrenceStrategy } from '../value-objects/RecurrenceStrategy';

import { RaceTimeOfDay } from '../value-objects/RaceTimeOfDay';
import type { Weekday } from '../types/Weekday';
import { weekdayToIndex } from '../types/Weekday';
import type { IDomainCalculationService } from '@core/shared/domain';
import { RacingDomainValidationError } from '../errors/RacingDomainError';

function cloneDate(date: Date): Date {
  return new Date(date.getTime());
}

function addDays(date: Date, days: number): Date {
  const d = cloneDate(date);
  d.setDate(d.getDate() + days);
  return d;
}

function addWeeks(date: Date, weeks: number): Date {
  return addDays(date, weeks * 7);
}

function addMonths(date: Date, months: number): Date {
  const d = cloneDate(date);
  const targetMonth = d.getMonth() + months;
  d.setMonth(targetMonth);
  return d;
}

function applyTimeOfDay(baseDate: Date, timeOfDay: RaceTimeOfDay): Date {
  const d = new Date(
    baseDate.getFullYear(),
    baseDate.getMonth(),
    baseDate.getDate(),
    timeOfDay.hour,
    timeOfDay.minute,
    0,
    0,
  );
  return d;
}

// Treat Monday as 1 ... Sunday as 7
function getCalendarWeekdayIndex(date: Date): number {
  const jsDay = date.getDay(); // 0=Sun ... 6=Sat
  if (jsDay === 0) {
    return 7;
  }
  return jsDay;
}

function weekdayToCalendarOffset(anchor: Date, target: Weekday): number {
  const anchorIndex = getCalendarWeekdayIndex(anchor);
  const targetIndex = weekdayToIndex(target);
  return targetIndex - anchorIndex;
}

function generateWeeklyOrEveryNWeeksSlots(
  schedule: SeasonSchedule,
  maxRounds: number,
): ScheduledRaceSlot[] {
  const result: ScheduledRaceSlot[] = [];
  const recurrence = schedule.recurrence;
  const weekdays =
    recurrence.props.kind === 'weekly' || recurrence.props.kind === 'everyNWeeks'
      ? recurrence.props.weekdays.getAll()
      : [];

  if (weekdays.length === 0) {
    throw new RacingDomainValidationError('RecurrenceStrategy has no weekdays');
  }

  const intervalWeeks = recurrence.props.kind === 'everyNWeeks' ? recurrence.props.intervalWeeks : 1;

  let anchorWeekStart = cloneDate(schedule.startDate);
  let roundNumber = 1;

  while (result.length < maxRounds) {
    for (const weekday of weekdays) {
      const offset = weekdayToCalendarOffset(anchorWeekStart, weekday);
      const candidateDate = addDays(anchorWeekStart, offset);

      if (candidateDate < schedule.startDate) {
        continue;
      }

      const scheduledAt = applyTimeOfDay(candidateDate, schedule.timeOfDay);
      result.push(
        new ScheduledRaceSlot({
          roundNumber,
          scheduledAt,
          timezone: schedule.timezone,
        }),
      );
      roundNumber += 1;

      if (result.length >= maxRounds) {
        break;
      }
    }

    anchorWeekStart = addWeeks(anchorWeekStart, intervalWeeks);
  }

  return result;
}

function findNthWeekdayOfMonth(base: Date, ordinal: 1 | 2 | 3 | 4, weekday: Weekday): Date {
  const firstOfMonth = new Date(base.getFullYear(), base.getMonth(), 1);
  const firstIndex = getCalendarWeekdayIndex(firstOfMonth);
  const targetIndex = weekdayToIndex(weekday);

  let offset = targetIndex - firstIndex;
  if (offset < 0) {
    offset += 7;
  }

  const dayOfMonth = 1 + offset + (ordinal - 1) * 7;
  return new Date(base.getFullYear(), base.getMonth(), dayOfMonth);
}

function generateMonthlySlots(schedule: SeasonSchedule, maxRounds: number): ScheduledRaceSlot[] {
  const result: ScheduledRaceSlot[] = [];
  const recurrence = schedule.recurrence;
  if (recurrence.props.kind !== 'monthlyNthWeekday') {
    return result;
  }

  const { ordinal, weekday } = recurrence.props.monthlyPattern;
  let currentMonthDate = new Date(
    schedule.startDate.getFullYear(),
    schedule.startDate.getMonth(),
    1,
  );
  let roundNumber = 1;

  while (result.length < maxRounds) {
    const candidateDate = findNthWeekdayOfMonth(currentMonthDate, ordinal, weekday);

    if (candidateDate >= schedule.startDate) {
      const scheduledAt = applyTimeOfDay(candidateDate, schedule.timeOfDay);
      result.push(
        new ScheduledRaceSlot({
          roundNumber,
          scheduledAt,
          timezone: schedule.timezone,
        }),
      );
      roundNumber += 1;
    }

    currentMonthDate = addMonths(currentMonthDate, 1);
  }

  return result;
}

export class SeasonScheduleGenerator {
  static generateSlots(schedule: SeasonSchedule): ScheduledRaceSlot[] {
    return this.generateSlotsUpTo(schedule, schedule.plannedRounds);
  }

  static generateSlotsUpTo(schedule: SeasonSchedule, maxRounds: number): ScheduledRaceSlot[] {
    if (!Number.isInteger(maxRounds) || maxRounds <= 0) {
      throw new RacingDomainValidationError('maxRounds must be a positive integer');
    }

    const recurrence: RecurrenceStrategy = schedule.recurrence;

    if (recurrence.props.kind === 'monthlyNthWeekday') {
      return generateMonthlySlots(schedule, maxRounds);
    }

    return generateWeeklyOrEveryNWeeksSlots(schedule, maxRounds);
  }
}

export class SeasonScheduleGeneratorService
  implements IDomainCalculationService<SeasonSchedule, ScheduledRaceSlot[]>
{
  calculate(schedule: SeasonSchedule): ScheduledRaceSlot[] {
    return SeasonScheduleGenerator.generateSlots(schedule);
  }
}