import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, IsNull, Not, MoreThanOrEqual, In } from 'typeorm';
import { Room } from '../entities/room.entity';
import { Student } from '../entities/student.entity';
import { Occupancy } from '../entities/occupancy.entity';
import { Bill } from '../entities/bill.entity';
import { RoomExpense } from '../entities/room-expense.entity';
import { Classroom } from '../entities/classroom.entity';
import { ClassSchedule } from '../entities/class-schedule.entity';
import { AttendanceRecord } from '../entities/attendance-record.entity';
import { Class } from '../entities/class.entity';
import { Deposit } from '../entities/deposit.entity';
import { ClassroomRental } from '../entities/classroom-rental.entity';
import { ClassTeacher } from '../entities/class-teacher.entity';
import { ClassStudent } from '../entities/class-student.entity';
import { DashboardQueriesService } from './dashboard-queries.service';

interface AgentAttendanceStatusRow {
  status: string;
  count: string | number;
}

@Injectable()
export class DashboardService {
  constructor(
    @InjectRepository(Room) private roomRepo: Repository<Room>,
    @InjectRepository(Student) private studentRepo: Repository<Student>,
    @InjectRepository(Occupancy) private occRepo: Repository<Occupancy>,
    @InjectRepository(Bill) private billRepo: Repository<Bill>,
    @InjectRepository(RoomExpense) private expRepo: Repository<RoomExpense>,
    @InjectRepository(Classroom) private classroomRepo: Repository<Classroom>,
    @InjectRepository(ClassSchedule) private scheduleRepo: Repository<ClassSchedule>,
    @InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
    @InjectRepository(Class) private classRepo: Repository<Class>,
    @InjectRepository(Deposit) private depositRepo: Repository<Deposit>,
    @InjectRepository(ClassroomRental) private rentalRepo: Repository<ClassroomRental>,
    @InjectRepository(ClassTeacher) private classTeacherRepo: Repository<ClassTeacher>,
    @InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
    private readonly queries: DashboardQueriesService,
  ) {}

  async getAccessibleClassIds(userId: number, canManageAll = false): Promise<number[] | undefined> {
    if (canManageAll) return undefined;
    const assignments = await this.classTeacherRepo.find({ where: { userId } });
    return [...new Set(assignments.map((assignment) => assignment.classId))];
  }

  async agentGetDashboardStats(userId: number, canManageAll: boolean) {
    const accessibleClassIds = await this.getAccessibleClassIds(userId, canManageAll);
    const today = this.getChinaDate(new Date());
    const totalStudents = accessibleClassIds
      ? await this.countStudentsInClasses(accessibleClassIds)
      : await this.studentRepo.count({ where: { status: 'active' } });
    const classCount = accessibleClassIds
      ? accessibleClassIds.length
      : await this.classRepo.count({ where: { isArchived: false } });
    const attendanceQb = this.attendanceRepo
      .createQueryBuilder('attendance')
      .select('attendance.status', 'status')
      .addSelect('COUNT(attendance.id)', 'count')
      .where('attendance.attendanceDate = :today', { today });
    this.applyClassScope(attendanceQb, 'attendance', accessibleClassIds);
    const rows = await attendanceQb
      .groupBy('attendance.status')
      .getRawMany<AgentAttendanceStatusRow>();
    const attendanceByStatus = rows.reduce(
      (result, row) => {
        result[String(row.status)] = Number(row.count || 0);
        return result;
      },
      {} as Record<string, number>,
    );
    const attendanceTotal = Object.values(attendanceByStatus).reduce<number>(
      (sum, count) => sum + Number(count),
      0,
    );
    const present = attendanceByStatus.present ?? 0;
    return {
      date: today,
      totalStudents,
      classCount,
      attendanceTotal,
      present,
      attendanceRate: attendanceTotal ? Number(((present / attendanceTotal) * 100).toFixed(1)) : 0,
      attendanceByStatus,
    };
  }

  async getStats(accessibleClassIds?: number[]) {
    const todayStr = this.getChinaDate(new Date());
    const currentMonth = todayStr.slice(0, 7); // YYYY-MM

    const totalRooms = await this.roomRepo.count({ where: { status: Not('archived') } });
    const totalStudents = accessibleClassIds
      ? await this.countStudentsInClasses(accessibleClassIds)
      : await this.studentRepo.count({ where: { status: 'active' } });
    const occupiedBeds = await this.occRepo.count({ where: { checkOutDate: IsNull() } });
    const capQb = this.roomRepo
      .createQueryBuilder('r')
      .select('SUM(r.capacity)', 'total')
      .where('r.status != :archived', { archived: 'archived' });
    const totalCapacity = await capQb.getRawOne();
    // MySQL 的 SUM() 聚合默认以字符串返回，需显式转成 number
    const cap = Number(totalCapacity?.total ?? 0) || 0;
    const occupancyRate = cap > 0 ? ((occupiedBeds / cap) * 100).toFixed(1) : 0;

    const billStatsQb = this.billRepo
      .createQueryBuilder('b')
      .select('b.status', 'status')
      .addSelect('COUNT(*)', 'count')
      .addSelect('SUM(b.totalAmount)', 'total')
      .groupBy('b.status');
    const billStats = await billStatsQb.getRawMany();

    // New fields
    const classroomCount = await this.classroomRepo.count({ where: {} });

    const occQb = this.scheduleRepo
      .createQueryBuilder('s')
      .select('COUNT(DISTINCT s.classroomId)', 'cnt')
      .where('s.status = :active', { active: 'active' })
      .andWhere('s.startDate <= :today', { today: todayStr })
      .andWhere('s.endDate >= :today', { today: todayStr });
    const occResult = await occQb.getRawOne();
    const occupiedClassrooms = parseInt(occResult?.cnt || '0', 10);
    const classroomOccupancyRate =
      classroomCount > 0 ? ((occupiedClassrooms / classroomCount) * 100).toFixed(1) : 0;

    const attTodayQb = this.attendanceRepo
      .createQueryBuilder('a')
      .select('a.status', 'status')
      .addSelect('COUNT(*)', 'count')
      .where('a.attendanceDate = :today', { today: todayStr });
    this.applyClassScope(attTodayQb, 'a', accessibleClassIds);
    attTodayQb.groupBy('a.status');
    const attTodayStats = await attTodayQb.getRawMany();
    const todayPresent = attTodayStats
      .filter((r) => r.status === 'present')
      .reduce((sum, r) => sum + parseInt(r.count, 10), 0);
    const incomeQb = this.billRepo
      .createQueryBuilder('b')
      .select('SUM(b.totalAmount)', 'total')
      .where('b.status = :paid', { paid: 'paid' })
      .andWhere('b.periodStart >= :start', { start: `${currentMonth}-01` })
      .andWhere('b.periodStart < :end', { end: this.nextMonth(currentMonth) });
    const incomeResult = await incomeQb.getRawOne();
    const monthlyIncome = parseFloat(incomeResult?.total || '0');

    const attendanceTrend = await this.getAttendanceTrend(todayStr, accessibleClassIds);
    const incomeTrend = await this.getIncomeTrend(currentMonth);

    const classCount = accessibleClassIds
      ? accessibleClassIds.length
      : await this.classRepo.count({ where: {} });

    const teacherResult = await this.classTeacherRepo
      .createQueryBuilder('ct')
      .select('COUNT(DISTINCT ct.userId)', 'cnt')
      .getRawOne();
    const teacherCount = parseInt(teacherResult?.cnt || '0', 10);

    const pendingQb = this.depositRepo
      .createQueryBuilder('d')
      .select('SUM(d.amount)', 'total')
      .where('d.status = :paid', { paid: 'paid' });
    const pendingResult = await pendingQb.getRawOne();
    const pendingDeposits = parseFloat(pendingResult?.total || '0');

    const activeRentals = await this.rentalRepo.count({
      where: { status: 'active' as const, endDate: MoreThanOrEqual(todayStr) },
    });

    const occByBldQb = this.occRepo
      .createQueryBuilder('o')
      .leftJoin('o.room', 'r')
      .select('r.building', 'building')
      .addSelect('COUNT(*)', 'count')
      .where('o.checkOutDate IS NULL');
    const occupancyByBuilding = await occByBldQb.groupBy('r.building').getRawMany();

    const attendanceByStatus = attTodayStats.reduce(
      (acc, r) => {
        acc[r.status] = parseInt(r.count, 10);
        return acc;
      },
      {} as Record<string, number>,
    );

    const expByTypeQb = this.expRepo
      .createQueryBuilder('e')
      .select('e.expenseType', 'type')
      .addSelect('SUM(e.amount)', 'total')
      .where('e.periodStart >= :start', { start: `${currentMonth}-01` })
      .andWhere('e.periodEnd <= :end', { end: this.nextMonth(currentMonth) });
    const expenseByType = await expByTypeQb.groupBy('e.expenseType').getRawMany();

    return {
      totalRooms,
      totalStudents,
      occupiedBeds,
      totalCapacity: cap,
      occupancyRate,
      billStats,
      classroomCount,
      classroomOccupancyRate,
      todayPresent,
      monthlyIncome,
      classCount,
      teacherCount,
      pendingDeposits,
      activeRentals,
      occupancyByBuilding,
      attendanceByStatus,
      expenseByType,
      attendanceTrend,
      incomeTrend,
    };
  }

  private applyClassScope(
    qb: { andWhere: (condition: string, parameters?: Record<string, unknown>) => unknown },
    alias: string,
    accessibleClassIds?: number[],
  ) {
    if (accessibleClassIds) {
      if (accessibleClassIds.length === 0) {
        qb.andWhere('1 = 0');
        return;
      }
      qb.andWhere(`${alias}.classId IN (:...accessibleClassIds)`, { accessibleClassIds });
    }
  }

  private async countStudentsInClasses(accessibleClassIds: number[]) {
    if (accessibleClassIds.length === 0) return 0;
    const classStudents = await this.classStudentRepo.find({
      where: { classId: In(accessibleClassIds), status: 'active' },
    });
    return new Set(classStudents.map((item) => item.studentId)).size;
  }

  async getAttendanceTrend(todayStr: string, accessibleClassIds?: number[]) {
    return this.queries.getAttendanceTrend(this.attendanceRepo, todayStr, accessibleClassIds);
  }

  async getIncomeTrend(currentMonth: string) {
    return this.queries.getIncomeTrend(this.billRepo, currentMonth);
  }

  async getGanttData(query?: { periodStart?: string; periodEnd?: string; building?: string }) {
    return this.queries.getGanttData(this.occRepo, (a, b) => this.assertPeriodRange(a, b), query);
  }

  async getExpenseStats(periodStart?: string, periodEnd?: string) {
    return this.queries.getExpenseStats(this.expRepo, (a, b) => this.assertPeriodRange(a, b), periodStart, periodEnd);
  }

  async getRoomExpenseRanking(periodStart?: string, periodEnd?: string) {
    return this.queries.getRoomExpenseRanking(this.expRepo, (a, b) => this.assertPeriodRange(a, b), periodStart, periodEnd);
  }

  async getClassAttendanceRanking(accessibleClassIds?: number[]) {
    return this.queries.getClassAttendanceRanking(
      this.attendanceRepo,
      (qb, alias, ids) => this.applyClassScope(qb, alias, ids),
      accessibleClassIds,
    );
  }

  private nextMonth(ym: string): string {
    const d = new Date(`${ym}-01`);
    d.setMonth(d.getMonth() + 1);
    return d.toISOString().slice(0, 7) + '-01';
  }

  async getClassroomOccupancy() {
    const classrooms = await this.classroomRepo.find({
      where: { status: 'available' as const },
      order: { building: 'ASC', name: 'ASC' },
    });
    const today = this.getChinaDate(new Date());
    const schedQb = this.scheduleRepo
      .createQueryBuilder('s')
      .select('s.classroomId', 'classroomId')
      .addSelect('COUNT(DISTINCT s.weekDay)', 'weekDays')
      .where('s.status = :active', { active: 'active' })
      .andWhere('s.scheduleType = :type', { type: 'INTERNAL' })
      .andWhere('s.startDate <= :today AND s.endDate >= :today', { today })
      .groupBy('s.classroomId');
    const schedules = await schedQb.getRawMany();
    const rentalQb = this.rentalRepo
      .createQueryBuilder('r')
      .select('r.classroomId', 'classroomId')
      .addSelect('COUNT(*)', 'rentalCount')
      .where('r.status = :active', { active: 'active' })
      .andWhere('r.startDate <= :today AND r.endDate >= :today', { today })
      .groupBy('r.classroomId');
    const rentals = await rentalQb.getRawMany();
    const sMap: Record<number, number> = {};
    const rMap: Record<number, number> = {};
    for (const s of schedules) sMap[s.classroomId] = parseInt(s.weekDays, 10);
    for (const r of rentals) rMap[r.classroomId] = parseInt(r.rentalCount, 10);
    return classrooms.map((c) => ({
      name: c.name,
      building: c.building || '',
      capacity: c.capacity,
      scheduleDays: sMap[c.id] || 0,
      rentalCount: rMap[c.id] || 0,
      occupancy: Math.min(((sMap[c.id] || 0) + (rMap[c.id] || 0) * 3) / 7, 1),
    }));
  }

  private assertPeriodRange(periodStart?: string, periodEnd?: string) {
    if (periodStart && periodEnd && periodStart > periodEnd) {
      throw new BadRequestException('结束日期不能早于开始日期');
    }
  }

  private getChinaDate(date: Date): string {
    const parts = new Intl.DateTimeFormat('en-CA', {
      timeZone: 'Asia/Shanghai',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
    }).formatToParts(date);
    const values = Object.fromEntries(
      parts.filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]),
    );
    return `${values.year}-${values.month}-${values.day}`;
  }

  async getClassroomUtilizationStats() {
    const totalClassrooms = await this.classroomRepo.count({
      where: { status: 'available' as const },
    });

    const today = this.getChinaDate(new Date());

    // Count classrooms with active schedules today
    const schedQb = this.scheduleRepo
      .createQueryBuilder('s')
      .select('COUNT(DISTINCT s.classroomId)', 'cnt')
      .where('s.status = :active', { active: 'active' })
      .andWhere('s.scheduleType = :type', { type: 'INTERNAL' })
      .andWhere('s.startDate <= :today AND s.endDate >= :today', { today });
    const schedResult = await schedQb.getRawOne();

    // Count classrooms with active rentals today
    const rentalQb = this.rentalRepo
      .createQueryBuilder('r')
      .select('COUNT(DISTINCT r.classroomId)', 'cnt')
      .where('r.status = :active', { active: 'active' })
      .andWhere('r.startDate <= :today AND r.endDate >= :today', { today });
    const rentalResult = await rentalQb.getRawOne();

    // Combine: use Set merge of both
    const combinedQb = this.scheduleRepo
      .createQueryBuilder('s')
      .select('s.classroomId')
      .where('s.status = :active', { active: 'active' })
      .andWhere('s.scheduleType = :type', { type: 'INTERNAL' })
      .andWhere('s.startDate <= :today AND s.endDate >= :today', { today })
      .groupBy('s.classroomId');
    const schedIds = await combinedQb.getRawMany();

    const combinedRentalQb = this.rentalRepo
      .createQueryBuilder('r')
      .select('r.classroomId')
      .where('r.status = :active', { active: 'active' })
      .andWhere('r.startDate <= :today AND r.endDate >= :today', { today })
      .groupBy('r.classroomId');
    const rentalIds = await combinedRentalQb.getRawMany();

    const allInUseIds = new Set([
      ...schedIds.map((s) => s.classroomId),
      ...rentalIds.map((r) => r.classroomId),
    ]);

    const scheduleCount = parseInt(schedResult?.cnt || '0', 10);
    const rentalCount = parseInt(rentalResult?.cnt || '0', 10);
    const inUseCount = allInUseIds.size;
    const utilizationRate =
      totalClassrooms > 0 ? ((inUseCount / totalClassrooms) * 100).toFixed(1) : '0';

    return {
      totalClassrooms,
      inUseCount,
      utilizationRate,
      scheduleCount,
      rentalCount,
    };
  }
}
