import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository, In } from 'typeorm';
import { Class, ClassStudent, ClassSchedule, AttendanceRecord } from '../entities';
import { Classroom } from '../entities/classroom.entity';
import { syncDingTalkStudents } from '../integration/dingtalk-student-sync';
import type { QueryClassScheduleDto, QueryClassAttendanceSummaryDto } from './dto/class.dto';

interface AgentClassRow {
  id: string | number;
  name: string;
  code: string;
  studentCount: string | number;
}

@Injectable()
export class ClassesQueriesService {
  constructor(
    @InjectRepository(Class) private readonly classRepo: Repository<Class>,
    @InjectRepository(ClassStudent) private readonly classStudentRepo: Repository<ClassStudent>,
    @InjectRepository(ClassSchedule) private readonly scheduleRepo: Repository<ClassSchedule>,
    @InjectRepository(AttendanceRecord) private readonly attendanceRepo: Repository<AttendanceRecord>,
    private readonly dataSource: DataSource,
  ) {}

  async agentSearchClasses(
    accessibleClassIds: number[] | undefined,
    query: { keyword?: string; status?: string; limit?: number },
  ) {
  if (accessibleClassIds?.length === 0) return [];

  const qb = this.classRepo
    .createQueryBuilder('class')
    .leftJoin(
      ClassStudent,
      'classStudent',
      'classStudent.classId = class.id AND classStudent.status = :activeStudent',
      { activeStudent: 'active' },
    )
    .select('class.id', 'id');
  const classSelects = [
    ['class.name', 'name'],
    ['class.code', 'code'],
    ['class.classType', 'classType'],
    ['class.status', 'status'],
    ['class.startDate', 'startDate'],
    ['class.endDate', 'endDate'],
    ['COUNT(classStudent.id)', 'studentCount'],
  ] as const;
  for (const [column, alias] of classSelects) {
    qb.addSelect(column, alias);
  }
  qb.where('class.isArchived = :isArchived', { isArchived: false });
  if (accessibleClassIds) qb.andWhere('class.id IN (:...accessibleClassIds)', { accessibleClassIds });
  if (query.keyword) qb.andWhere('(class.name LIKE :keyword OR class.code LIKE :keyword)', { keyword: `%${query.keyword}%` });
  if (query.status) qb.andWhere('class.status = :status', { status: query.status });
  const rows = await qb.groupBy('class.id').orderBy('class.name', 'ASC').limit(query.limit ?? 20).getRawMany<AgentClassRow>();
  return rows.map((row) => ({ ...row, id: Number(row.id), studentCount: Number(row.studentCount || 0) }));
  }

  async batchImportStudents(
    classId: number,
    users: Array<{ dingUserId: string; name: string; mobile?: string }>,
  ): Promise<{ imported: number; skipped: number; conflicts: number }> {
  if (users.length === 0) return { imported: 0, skipped: 0, conflicts: 0 };

  return this.dataSource.transaction(async (manager) => {
    const classEntity = await manager.findOne(Class, { where: { id: classId } });
    if (!classEntity) throw new NotFoundException('班级不存在');

    const synced = await syncDingTalkStudents(manager, users);
    const studentIds = [...new Set(synced.studentIds.values())];
    if (studentIds.length === 0) {
      return { imported: 0, skipped: 0, conflicts: synced.conflicts.length };
    }

    const existingClassStudents = await manager.find(ClassStudent, {
      where: { classId, studentId: In(studentIds) },
    });
    const existingByStudentId = new Map(
      existingClassStudents.map((classStudent) => [classStudent.studentId, classStudent]),
    );
    const today = new Date().toISOString().slice(0, 10);
    let skipped = 0;
    const memberships = studentIds.flatMap((studentId) => {
      const existing = existingByStudentId.get(studentId);
      if (existing?.status === 'active') {
        skipped++;
        return [];
      }
      if (existing) {
        existing.status = 'active';
        existing.joinDate = today;
        existing.leaveDate = null;
        return [existing];
      }
      return [
        manager.create(ClassStudent, {
          classId,
          studentId,
          status: 'active',
          joinDate: today,
        }),
      ];
    });

    if (memberships.length > 0) await manager.save(ClassStudent, memberships);
    return {
      imported: memberships.length,
      skipped,
      conflicts: synced.conflicts.length,
    };
  });
}

  async getSchedule(classId: number, query: QueryClassScheduleDto) {
  const qb = this.scheduleRepo
    .createQueryBuilder('cs')
    .leftJoinAndSelect('cs.classroom', 'classroom')
    .where('cs.classId = :classId', { classId });

  if (query.startDate) {
    qb.andWhere('cs.endDate >= :startDate', { startDate: query.startDate });
  }
  if (query.endDate) {
    qb.andWhere('cs.startDate <= :endDate', { endDate: query.endDate });
  }

  const schedules = await qb
    .orderBy('cs.weekDay', 'ASC')
    .addOrderBy('cs.startTime', 'ASC')
    .getMany();

  return schedules.map((s) => ({
    ...s,
    classroomName: (s.classroom as Classroom | undefined)?.name || null,
  }));
}

  async getAttendanceSummary(classId: number, query: QueryClassAttendanceSummaryDto) {
    const qb = this.attendanceRepo
      .createQueryBuilder('ar')
      .where('ar.classId = :classId', { classId });

    if (query.startDate) {
      qb.andWhere('ar.attendanceDate >= :startDate', { startDate: query.startDate });
    }
    if (query.endDate) {
      qb.andWhere('ar.attendanceDate <= :endDate', { endDate: query.endDate });
    }

    const rows = await qb.getMany();

    const total = rows.length;
    const present = rows.filter((r) => r.status === 'present').length;
    const late = rows.filter((r) => r.status === 'late').length;
    const absent = rows.filter((r) => r.status === 'absent').length;
    const leave = rows.filter((r) => r.status === 'leave').length;

    return {
      total,
      present,
      late,
      absent,
      leave,
      presentRate: total > 0 ? Number(((present / total) * 100).toFixed(1)) : 0,
      absentRate: total > 0 ? Number(((absent / total) * 100).toFixed(1)) : 0,
      lateRate: total > 0 ? Number(((late / total) * 100).toFixed(1)) : 0,
      leaveRate: total > 0 ? Number(((leave / total) * 100).toFixed(1)) : 0,
    };
  }
}
