import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, EntityManager, In, Like, Repository } from 'typeorm';
import { Class, ClassStudent, ClassTeacher, Exam, ExamScore } from '../entities';
import { CreateExamDto, QueryExamDto } from './dto/exam.dto';

@Injectable()
export class ExamsService {
  constructor(
    @InjectRepository(Exam) private examRepo: Repository<Exam>,
    @InjectRepository(ExamScore) private scoreRepo: Repository<ExamScore>,
    @InjectRepository(Class) private classRepo: Repository<Class>,
    @InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
    @InjectRepository(ClassTeacher) private classTeacherRepo: Repository<ClassTeacher>,
    private dataSource: DataSource,
  ) {}

  async getAccessibleClassIds(userId: number, canManageAll: boolean) {
    if (canManageAll) return undefined;
    const rows = await this.classTeacherRepo.find({ where: { userId } });
    return [...new Set(rows.map((row) => row.classId))];
  }

  async assertClassAccess(userId: number, classId: number, canManageAll: boolean) {
    if (canManageAll) return;
    const assignment = await this.classTeacherRepo.findOne({ where: { userId, classId } });
    if (!assignment) throw new ForbiddenException('只能访问自己被分配的班级');
  }

  async findAll(query: QueryExamDto, accessibleClassIds?: number[]) {
    const where: Record<string, unknown> = {
      status: 'active',
    };
    if (query.keyword) where.examName = Like(`%${query.keyword}%`);
    if (query.examType) where.examType = query.examType;
    if (query.classId) where.classId = query.classId;
    if (accessibleClassIds) {
      if (accessibleClassIds.length === 0) return [];
      where.classId = query.classId
        ? accessibleClassIds.includes(query.classId)
          ? query.classId
          : -1
        : In(accessibleClassIds);
    }

    const exams = await this.examRepo.find({
      where,
      relations: ['class'],
      order: { examDate: 'DESC', createdAt: 'DESC' },
    });
    if (exams.length === 0) return [];
    const scoreRows = await this.scoreRepo.find({
      where: { examId: In(exams.map((exam) => exam.id)), status: 'active' },
    });
    const progress = new Map<number, { total: number; entered: number }>();
    for (const row of scoreRows) {
      const item = progress.get(row.examId!) ?? { total: 0, entered: 0 };
      item.total++;
      if (row.score !== null && row.score !== undefined) item.entered++;
      progress.set(row.examId!, item);
    }
    return exams.map((exam) => ({
      ...exam,
      className: exam.class?.name,
      totalStudents: progress.get(exam.id)?.total ?? 0,
      enteredScores: progress.get(exam.id)?.entered ?? 0,
    }));
  }

  async findOne(id: number, userId: number, canManageAll: boolean) {
    const exam = await this.examRepo.findOne({ where: { id }, relations: ['class'] });
    if (!exam) throw new NotFoundException('考试不存在');
    await this.assertClassAccess(userId, exam.classId, canManageAll);
    const scores = await this.scoreRepo.find({
      where: { examId: id, status: 'active' },
      relations: ['student'],
      order: { id: 'ASC' },
    });
    return {
      ...exam,
      className: exam.class?.name,
      totalStudents: scores.length,
      enteredScores: scores.filter((row) => row.score !== null && row.score !== undefined).length,
      scores: scores.map((row) => ({
        id: row.id,
        studentId: row.studentId,
        phone: row.student?.phone,
        name: row.student?.name,
        score: row.score === null ? null : Number(row.score),
        classAvg: row.classAvg === null ? null : Number(row.classAvg),
        rank: row.rank,
      })),
    };
  }

  async create(dto: CreateExamDto, userId: number, canManageAll: boolean) {
    await this.assertClassAccess(userId, dto.classId, canManageAll);
    return this.dataSource.transaction(async (manager) => {
      const cls = await manager.findOne(Class, { where: { id: dto.classId } });
      if (!cls) throw new NotFoundException('班级不存在');
      if (cls.isArchived) throw new BadRequestException('归档班级不能创建考试');
      const members = await manager.find(ClassStudent, {
        where: { classId: dto.classId, status: 'active' },
        order: { createdAt: 'ASC' },
      });
      if (members.length === 0) throw new BadRequestException('班级暂无在读学员，不能创建考试');
      const exam = await manager.save(Exam, manager.create(Exam, { ...dto, status: 'active' }));
      await this.createScoreRows(manager, exam, members);
      return exam;
    });
  }

  async updateScore(
    examId: number,
    scoreId: number,
    score: number | null | undefined,
    userId: number,
    canManageAll: boolean,
  ) {
    return this.dataSource.transaction(async (manager) => {
      const exam = await manager.findOne(Exam, { where: { id: examId } });
      if (!exam) throw new NotFoundException('考试不存在');
      await this.assertClassAccess(userId, exam.classId, canManageAll);
      if (exam.status === 'archived') throw new BadRequestException('已归档考试不能录入成绩');
      const row = await manager.findOne(ExamScore, { where: { id: scoreId, examId } });
      if (!row) throw new NotFoundException('成绩记录不存在');
      row.score = score === undefined ? null : score;
      await manager.save(ExamScore, row);
      await this.recalculate(manager, examId);
      return manager.findOne(ExamScore, { where: { id: scoreId } });
    });
  }

  private async createScoreRows(manager: EntityManager, exam: Exam, members: ClassStudent[]) {
    const rows = members.map((member) =>
      manager.create(ExamScore, {
        examId: exam.id,
        studentId: member.studentId,
        examType: exam.examType,
        examName: exam.examName,
        subject: exam.subject,
        score: null,
        classAvg: null,
        rank: null,
        examDate: exam.examDate,
        status: 'active',
      }),
    );
    await manager.save(ExamScore, rows);
  }

  private async recalculate(manager: EntityManager, examId: number) {
    const rows = await manager.find(ExamScore, { where: { examId, status: 'active' } });
    const entered = rows
      .filter((row) => row.score !== null && row.score !== undefined)
      .sort((a, b) => Number(b.score) - Number(a.score));
    const average =
      entered.length === 0
        ? null
        : Math.round((entered.reduce((sum, row) => sum + Number(row.score), 0) / entered.length) * 100) /
          100;
    let previousScore: number | null = null;
    let previousRank = 0;
    for (let index = 0; index < entered.length; index++) {
      const row = entered[index];
      const value = Number(row.score);
      const rank = previousScore !== null && value === previousScore ? previousRank : index + 1;
      row.classAvg = average;
      row.rank = rank;
      previousScore = value;
      previousRank = rank;
    }
    const enteredIds = new Set(entered.map((row) => row.id));
    for (const row of rows) {
      if (!enteredIds.has(row.id)) {
        row.classAvg = average;
        row.rank = null;
      }
    }
    await manager.save(ExamScore, rows);
  }
}
