import { Season } from '@core/racing/domain/entities/season/Season';
import { ALL_WEEKDAYS, type Weekday } from '@core/racing/domain/types/Weekday';
import { LeagueTimezone } from '@core/racing/domain/value-objects/LeagueTimezone';
import { MonthlyRecurrencePattern } from '@core/racing/domain/value-objects/MonthlyRecurrencePattern';
import { RaceTimeOfDay } from '@core/racing/domain/value-objects/RaceTimeOfDay';
import type { RecurrenceStrategy } from '@core/racing/domain/value-objects/RecurrenceStrategy';
import { RecurrenceStrategyFactory } from '@core/racing/domain/value-objects/RecurrenceStrategyFactory';
import { SeasonDropPolicy } from '@core/racing/domain/value-objects/SeasonDropPolicy';
import { SeasonSchedule } from '@core/racing/domain/value-objects/SeasonSchedule';
import { SeasonScoringConfig } from '@core/racing/domain/value-objects/SeasonScoringConfig';
import { SeasonStatus, type SeasonStatusValue } from '@core/racing/domain/value-objects/SeasonStatus';
import { SeasonStewardingConfig } from '@core/racing/domain/value-objects/SeasonStewardingConfig';
import { WeekdaySet } from '@core/racing/domain/value-objects/WeekdaySet';

import { SeasonOrmEntity } from '../entities/SeasonOrmEntity';
import { TypeOrmPersistenceSchemaError } from '../errors/TypeOrmPersistenceSchemaError';
import {
    assertArray,
    assertEnumValue,
    assertInteger,
    assertIsoDate,
    assertNonEmptyString,
    assertRecord,
} from '../schema/TypeOrmSchemaGuards';
import type {
    SerializedSeasonDropPolicy,
    SerializedSeasonEveryNWeeksRecurrence,
    SerializedSeasonMonthlyNthWeekdayRecurrence,
    SerializedSeasonSchedule,
    SerializedSeasonScoringConfig,
    SerializedSeasonStewardingConfig,
    SerializedSeasonWeeklyRecurrence,
} from '../serialized/RacingTypeOrmSerialized';

const VALID_SEASON_STATUSES = [
  'planned',
  'active',
  'completed',
  'archived',
  'cancelled',
] as const satisfies readonly SeasonStatusValue[];

const VALID_RECURRENCE_KINDS = ['weekly', 'everyNWeeks', 'monthlyNthWeekday'] as const;

function isMonthlyOrdinal(value: number): value is 1 | 2 | 3 | 4 {
  return value === 1 || value === 2 || value === 3 || value === 4;
}

function parseSeasonSchedule(value: unknown): SeasonSchedule {
  const entityName = 'Season';

  assertRecord(entityName, 'schedule', value);

  const startDateCandidate = value.startDate;
  assertIsoDate(entityName, 'schedule.startDate', startDateCandidate);
  const startDate = new Date(startDateCandidate);

  const timeOfDayCandidate = value.timeOfDay;
  assertNonEmptyString(entityName, 'schedule.timeOfDay', timeOfDayCandidate);

  const timezoneIdCandidate = value.timezoneId;
  assertNonEmptyString(entityName, 'schedule.timezoneId', timezoneIdCandidate);

  const plannedRoundsCandidate = value.plannedRounds;
  assertInteger(entityName, 'schedule.plannedRounds', plannedRoundsCandidate);
  if (plannedRoundsCandidate <= 0) {
    throw new TypeOrmPersistenceSchemaError({
      entityName,
      fieldName: 'schedule.plannedRounds',
      reason: 'invalid_shape',
      message: 'Invalid schedule.plannedRounds (expected positive integer)',
    });
  }
  const plannedRounds = plannedRoundsCandidate;

  const recurrenceCandidate = value.recurrence;
  assertRecord(entityName, 'schedule.recurrence', recurrenceCandidate);

  const recurrenceKindCandidate = recurrenceCandidate.kind;
  assertEnumValue(entityName, 'schedule.recurrence.kind', recurrenceKindCandidate, VALID_RECURRENCE_KINDS);
  const recurrenceKind = recurrenceKindCandidate;

  let timeOfDay: RaceTimeOfDay;
  try {
    timeOfDay = RaceTimeOfDay.fromString(timeOfDayCandidate);
  } catch {
    throw new TypeOrmPersistenceSchemaError({
      entityName,
      fieldName: 'schedule.timeOfDay',
      reason: 'invalid_shape',
    });
  }

  let timezone: LeagueTimezone;
  try {
    timezone = LeagueTimezone.create(timezoneIdCandidate);
  } catch {
    throw new TypeOrmPersistenceSchemaError({
      entityName,
      fieldName: 'schedule.timezoneId',
      reason: 'invalid_shape',
    });
  }

  const recurrenceRecord = recurrenceCandidate;

  const recurrence: RecurrenceStrategy = (() => {
    switch (recurrenceKind) {
      case 'weekly': {
        const weekdaysCandidate = recurrenceRecord.weekdays;
        assertArray(entityName, 'schedule.recurrence.weekdays', weekdaysCandidate);

        const typedWeekdays: Weekday[] = [];
        for (let index = 0; index < weekdaysCandidate.length; index += 1) {
          const dayCandidate: unknown = weekdaysCandidate[index];
          assertEnumValue(entityName, `schedule.recurrence.weekdays[${index}]`, dayCandidate, ALL_WEEKDAYS);
          typedWeekdays.push(dayCandidate);
        }

        try {
          return RecurrenceStrategyFactory.weekly(WeekdaySet.fromArray(typedWeekdays));
        } catch {
          throw new TypeOrmPersistenceSchemaError({
            entityName,
            fieldName: 'schedule.recurrence',
            reason: 'invalid_shape',
          });
        }
      }

      case 'everyNWeeks': {
        const intervalWeeksCandidate = recurrenceRecord.intervalWeeks;
        assertInteger(entityName, 'schedule.recurrence.intervalWeeks', intervalWeeksCandidate);
        if (intervalWeeksCandidate <= 0) {
          throw new TypeOrmPersistenceSchemaError({
            entityName,
            fieldName: 'schedule.recurrence.intervalWeeks',
            reason: 'invalid_shape',
            message: 'Invalid schedule.recurrence.intervalWeeks (expected positive integer)',
          });
        }

        const weekdaysCandidate = recurrenceRecord.weekdays;
        assertArray(entityName, 'schedule.recurrence.weekdays', weekdaysCandidate);

        const typedWeekdays: Weekday[] = [];
        for (let index = 0; index < weekdaysCandidate.length; index += 1) {
          const dayCandidate: unknown = weekdaysCandidate[index];
          assertEnumValue(entityName, `schedule.recurrence.weekdays[${index}]`, dayCandidate, ALL_WEEKDAYS);
          typedWeekdays.push(dayCandidate);
        }

        try {
          return RecurrenceStrategyFactory.everyNWeeks(intervalWeeksCandidate, WeekdaySet.fromArray(typedWeekdays));
        } catch {
          throw new TypeOrmPersistenceSchemaError({
            entityName,
            fieldName: 'schedule.recurrence',
            reason: 'invalid_shape',
          });
        }
      }

      case 'monthlyNthWeekday': {
        const ordinalCandidate = recurrenceRecord.ordinal;
        assertInteger(entityName, 'schedule.recurrence.ordinal', ordinalCandidate);
        if (!isMonthlyOrdinal(ordinalCandidate)) {
          throw new TypeOrmPersistenceSchemaError({
            entityName,
            fieldName: 'schedule.recurrence.ordinal',
            reason: 'invalid_enum_value',
          });
        }

        const weekdayCandidate = recurrenceRecord.weekday;
        assertEnumValue(entityName, 'schedule.recurrence.weekday', weekdayCandidate, ALL_WEEKDAYS);

        try {
          const pattern = MonthlyRecurrencePattern.create(ordinalCandidate, weekdayCandidate);
          return RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
        } catch {
          throw new TypeOrmPersistenceSchemaError({
            entityName,
            fieldName: 'schedule.recurrence',
            reason: 'invalid_shape',
          });
        }
      }
    }
  })();

  return new SeasonSchedule({
    startDate,
    timeOfDay,
    timezone,
    recurrence,
    plannedRounds,
  });
}

function serializeSeasonSchedule(schedule: SeasonSchedule): SerializedSeasonSchedule {
  const recurrence = (() => {
    const props = schedule.recurrence.props;
    switch (props.kind) {
      case 'weekly':
        return {
          kind: 'weekly',
          weekdays: props.weekdays.getAll(),
        } satisfies SerializedSeasonWeeklyRecurrence;
      case 'everyNWeeks':
        return {
          kind: 'everyNWeeks',
          intervalWeeks: props.intervalWeeks,
          weekdays: props.weekdays.getAll(),
        } satisfies SerializedSeasonEveryNWeeksRecurrence;
      case 'monthlyNthWeekday':
        return {
          kind: 'monthlyNthWeekday',
          ordinal: props.monthlyPattern.ordinal,
          weekday: props.monthlyPattern.weekday,
        } satisfies SerializedSeasonMonthlyNthWeekdayRecurrence;
    }
  })();

  return {
    startDate: schedule.startDate.toISOString(),
    timeOfDay: schedule.timeOfDay.toString(),
    timezoneId: schedule.timezone.toString(),
    recurrence,
    plannedRounds: schedule.plannedRounds,
  };
}

function serializeSeasonScoringConfig(config: SeasonScoringConfig): SerializedSeasonScoringConfig {
  const props = config.props;
  return {
    scoringPresetId: props.scoringPresetId,
    ...(props.customScoringEnabled ? { customScoringEnabled: props.customScoringEnabled } : {}),
  };
}

function serializeSeasonDropPolicy(policy: SeasonDropPolicy): SerializedSeasonDropPolicy {
  const props = policy.props;
  return {
    strategy: props.strategy,
    ...(props.n !== undefined ? { n: props.n } : {}),
  };
}

function serializeSeasonStewardingConfig(config: SeasonStewardingConfig): SerializedSeasonStewardingConfig {
  const props = config.props;
  return {
    decisionMode: props.decisionMode,
    ...(props.requiredVotes !== undefined ? { requiredVotes: props.requiredVotes } : {}),
    requireDefense: props.requireDefense,
    defenseTimeLimit: props.defenseTimeLimit,
    voteTimeLimit: props.voteTimeLimit,
    protestDeadlineHours: props.protestDeadlineHours,
    stewardingClosesHours: props.stewardingClosesHours,
    notifyAccusedOnProtest: props.notifyAccusedOnProtest,
    notifyOnVoteRequired: props.notifyOnVoteRequired,
  };
}

export class SeasonOrmMapper {
  toOrmEntity(domain: Season): SeasonOrmEntity {
    const entity = new SeasonOrmEntity();
    entity.id = domain.id;
    entity.leagueId = domain.leagueId;
    entity.gameId = domain.gameId;
    entity.name = domain.name;
    entity.year = domain.year ?? null;
    entity.order = domain.order ?? null;
    entity.status = domain.status.toString();
    entity.startDate = domain.startDate ?? null;
    entity.endDate = domain.endDate ?? null;
    entity.schedule = domain.schedule ? serializeSeasonSchedule(domain.schedule) : null;
    entity.schedulePublished = domain.schedulePublished;
    entity.scoringConfig = domain.scoringConfig ? serializeSeasonScoringConfig(domain.scoringConfig) : null;
    entity.dropPolicy = domain.dropPolicy ? serializeSeasonDropPolicy(domain.dropPolicy) : null;
    entity.stewardingConfig = domain.stewardingConfig ? serializeSeasonStewardingConfig(domain.stewardingConfig) : null;
    entity.maxDrivers = domain.maxDrivers ?? null;
    entity.participantCount = domain.getParticipantCount();
    return entity;
  }

  toDomain(entity: SeasonOrmEntity): Season {
    const entityName = 'Season';

    const statusValue = entity.status;
    assertEnumValue(entityName, 'status', statusValue, VALID_SEASON_STATUSES);

    let status: SeasonStatus;
    try {
      status = SeasonStatus.create(statusValue);
    } catch {
      throw new TypeOrmPersistenceSchemaError({
        entityName,
        fieldName: 'status',
        reason: 'invalid_enum_value',
      });
    }

    const schedule = entity.schedule !== null ? parseSeasonSchedule(entity.schedule) : undefined;

    let scoringConfig: SeasonScoringConfig | undefined;
    if (entity.scoringConfig !== null) {
      assertRecord(entityName, 'scoringConfig', entity.scoringConfig);
      try {
        scoringConfig = new SeasonScoringConfig(entity.scoringConfig);
      } catch {
        throw new TypeOrmPersistenceSchemaError({
          entityName,
          fieldName: 'scoringConfig',
          reason: 'invalid_shape',
        });
      }
    }

    let dropPolicy: SeasonDropPolicy | undefined;
    if (entity.dropPolicy !== null) {
      assertRecord(entityName, 'dropPolicy', entity.dropPolicy);
      try {
        dropPolicy = new SeasonDropPolicy(entity.dropPolicy);
      } catch {
        throw new TypeOrmPersistenceSchemaError({
          entityName,
          fieldName: 'dropPolicy',
          reason: 'invalid_shape',
        });
      }
    }

    let stewardingConfig: SeasonStewardingConfig | undefined;
    if (entity.stewardingConfig !== null) {
      assertRecord(entityName, 'stewardingConfig', entity.stewardingConfig);
      try {
        stewardingConfig = new SeasonStewardingConfig(entity.stewardingConfig);
      } catch {
        throw new TypeOrmPersistenceSchemaError({
          entityName,
          fieldName: 'stewardingConfig',
          reason: 'invalid_shape',
        });
      }
    }

    return Season.rehydrate({
      id: entity.id,
      leagueId: entity.leagueId,
      gameId: entity.gameId,
      name: entity.name,
      ...(entity.year !== null ? { year: entity.year } : {}),
      ...(entity.order !== null ? { order: entity.order } : {}),
      status,
      ...(entity.startDate !== null ? { startDate: entity.startDate } : {}),
      ...(entity.endDate !== null ? { endDate: entity.endDate } : {}),
      ...(schedule !== undefined ? { schedule } : {}),
      schedulePublished: entity.schedulePublished,
      ...(scoringConfig !== undefined ? { scoringConfig } : {}),
      ...(dropPolicy !== undefined ? { dropPolicy } : {}),
      ...(stewardingConfig !== undefined ? { stewardingConfig } : {}),
      ...(entity.maxDrivers !== null ? { maxDrivers: entity.maxDrivers } : {}),
      participantCount: entity.participantCount,
    });
  }
}