import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { In, LessThan, LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm';
import { AttendanceSession, ClassSchedule, ScheduleType } from '../entities';
import { AttendanceImportService } from './attendance-import.service';
import { AttendanceService } from './attendance.service';

@Injectable()
export class AttendanceSettlementService {
  private readonly logger = new Logger(AttendanceSettlementService.name);
  private readonly courseTimeZone = 'Asia/Shanghai';
  private running = false;

  constructor(
    @InjectRepository(ClassSchedule)
    private readonly scheduleRepo: Repository<ClassSchedule>,
    @InjectRepository(AttendanceSession)
    private readonly sessionRepo: Repository<AttendanceSession>,
    private readonly attendanceService: AttendanceService,
    private readonly importService: AttendanceImportService,
  ) {}

  @Cron('* * * * *')
  async settleEndedLessons(now = new Date()): Promise<void> {
    if (this.running) return;
    this.running = true;
    try {
      const clock = this.getCourseClock(now);
      const staleClaimBefore = new Date(now.getTime() - 35 * 60 * 1000);
      await this.sessionRepo.update(
        { status: 'settling', updatedAt: LessThan(staleClaimBefore) },
        { status: 'in_progress' },
      );
      const today = clock.date;
      const yesterday = this.shiftDate(today, -1);
      const [schedules, sessions] = await Promise.all([
        this.scheduleRepo.find({
          where: {
            scheduleType: ScheduleType.INTERNAL,
            status: 'active',
            startDate: LessThanOrEqual(today),
            endDate: MoreThanOrEqual(yesterday),
          },
        }),
        this.sessionRepo.find({
          where: { status: In(['in_progress', 'settling']) },
          relations: ['schedule'],
        }),
      ]);
      const sessionByKey = new Map(
        sessions.map((session) => [`${session.scheduleId}|${session.lessonDate}`, session]),
      );
      const candidates = new Map<string, { schedule: ClassSchedule; lessonDate: string; session?: AttendanceSession }>();

      for (const schedule of schedules) {
        const lessonDate = this.getEndedOccurrenceDate(schedule, clock, today, yesterday);
        if (lessonDate) {
          const key = `${schedule.id}|${lessonDate}`;
          candidates.set(key, { schedule, lessonDate, session: sessionByKey.get(key) });
        }
      }
      for (const session of sessions) {
        if (
          session.status === 'in_progress' &&
          session.schedule &&
          this.hasOccurrenceEnded(session.schedule, session.lessonDate, clock)
        ) {
          candidates.set(`${session.scheduleId}|${session.lessonDate}`, {
            schedule: session.schedule,
            lessonDate: session.lessonDate,
            session,
          });
        }
      }

      for (const candidate of candidates.values()) {
        await this.settleCandidate(candidate);
      }
    } finally {
      this.running = false;
    }
  }

  private async settleCandidate(candidate: {
    schedule: ClassSchedule;
    lessonDate: string;
    session?: AttendanceSession;
  }): Promise<void> {
    const { schedule, lessonDate } = candidate;
    if (schedule.classId == null || schedule.teacherId == null) {
      this.logger.error(`课程${schedule.id} ${lessonDate}缺少班级或教师，无法自动结算`);
      return;
    }

    let session = candidate.session;
    try {
      if (!session) {
        const created = await this.attendanceService.createLessonAttendanceFromDingTalk(
          schedule.id,
          lessonDate,
          schedule.teacherId,
          false,
        );
        session = created.session;
      }
      const claimed = await this.sessionRepo.update(
        { id: session.id, status: 'in_progress' },
        { status: 'settling' },
      );
      if (claimed.affected !== 1) return;

      const userIds = await this.attendanceService.getTeacherClassDingUserIds(
        schedule.teacherId,
        schedule.classId,
        false,
        lessonDate,
      );
      const importRange = this.attendanceService.getLessonAttendanceImportDateRange(
        schedule,
        lessonDate,
      );
      const imported = await this.importService.importFromDingTalk({
        ...importRange,
        userIds,
        autoMatch: true,
        userId: schedule.teacherId,
      });
      if (!imported.success || imported.errors.length > 0) {
        throw new Error(imported.errors.join('; ') || '钉钉考勤拉取失败');
      }
      await this.attendanceService.createLessonAttendanceFromDingTalk(
        schedule.id,
        lessonDate,
        schedule.teacherId,
        true,
      );
    } catch (error: unknown) {
      if (session) await this.sessionRepo.update({ id: session.id, status: 'settling' }, { status: 'in_progress' });
      this.logger.error(
        `课程${schedule.id} ${lessonDate}自动结算失败: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }

  private getEndedOccurrenceDate(
    schedule: ClassSchedule,
    clock: { weekDay: number; minutes: number },
    today: string,
    yesterday: string,
  ): string | null {
    const endMinutes = this.toMinutes(schedule.endTime);
    const overnight = this.isOvernight(schedule);
    const yesterdayWeekDay = clock.weekDay === 1 ? 7 : clock.weekDay - 1;
    if (
      !overnight &&
      schedule.weekDay === clock.weekDay &&
      clock.minutes >= endMinutes &&
      today >= schedule.startDate &&
      today <= schedule.endDate
    ) return today;
    if (
      overnight &&
      schedule.weekDay === yesterdayWeekDay &&
      clock.minutes >= endMinutes &&
      yesterday >= schedule.startDate &&
      yesterday <= schedule.endDate
    ) return yesterday;
    return null;
  }

  private hasOccurrenceEnded(
    schedule: ClassSchedule,
    lessonDate: string,
    clock: { date: string; minutes: number },
  ): boolean {
    const occurrenceEndDate = this.isOvernight(schedule)
      ? this.shiftDate(lessonDate, 1)
      : lessonDate;
    if (clock.date !== occurrenceEndDate) return clock.date > occurrenceEndDate;
    return clock.minutes >= this.toMinutes(schedule.endTime);
  }

  private isOvernight(schedule: ClassSchedule): boolean {
    return this.toMinutes(schedule.endTime) <= this.toMinutes(schedule.startTime);
  }

  private toMinutes(time: string): number {
    const [hour, minute] = time.split(':').map(Number);
    return hour * 60 + minute;
  }

  private getCourseClock(date: Date): { date: string; weekDay: number; minutes: number } {
    const parts = Object.fromEntries(
      new Intl.DateTimeFormat('en-CA', {
        timeZone: this.courseTimeZone,
        year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'short',
        hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
      }).formatToParts(date).filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]),
    );
    const weekDays: Record<string, number> = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
    return {
      date: `${parts.year}-${parts.month}-${parts.day}`,
      weekDay: weekDays[parts.weekday],
      minutes: Number(parts.hour) * 60 + Number(parts.minute),
    };
  }

  private shiftDate(date: string, days: number): string {
    const shifted = new Date(`${date}T00:00:00.000Z`);
    shifted.setUTCDate(shifted.getUTCDate() + days);
    return shifted.toISOString().slice(0, 10);
  }
}
