import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Sse,
  Body,
  Param,
  Query,
  UseGuards,
  Request,
  Res,
  BadRequestException,
  ForbiddenException,
  ParseIntPipe,
} from '@nestjs/common';
import { Observable, filter } from 'rxjs';
import type { Request as ExpressRequest, Response } from 'express';
import { AttendanceService } from './attendance.service';
import { AttendanceImportService } from './attendance-import.service';
import { DingTalkImportDto } from './dto/dingtalk-import.dto';
import {
  BatchCreateAttendanceDto,
  AttendanceSummaryQueryDto,
  AttendanceCalendarQueryDto,
  QueryAttendanceRecordsDto,
  AttendanceScheduleOptionsQueryDto,
  QueryDingRawDto,
  MatchDingRecordDto,
  AttendanceReportQueryDto,
  AttendanceAlertsQueryDto,
  UpdateAttendanceRecordDto,
  GenerateFromSchedulesDto,
  LessonAttendanceQueryDto,
  StartLessonAttendanceDto,
  SaveAttendancePeriodConfigsDto,
  RefreshDingTalkAttendanceDto,
} from './dto/attendance.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
import { extractRequestInfo } from '../common/request-utils';
import { RequirePermission } from '../auth/decorators/permission.decorator';
import * as ExcelJS from 'exceljs';
import { AuthorizationService, CaslAction, SubjectName } from '../authorization';
import type { AuthenticatedUser } from '../authorization';

/** SSE event shape for @Sse() decorator */
interface SseEvent {
  data: string | Record<string, unknown>;
  id?: string;
  type?: string;
  retry?: number;
}
/** Minimal request user shape for type safety */
interface RequestUser {
  id: number;
  username: string;
  permissions: string[];
  isSuperAdmin: boolean;
  roles: string[];
}

@UseGuards(JwtAuthGuard)
@Controller()
export class AttendanceController {
  constructor(
    private readonly service: AttendanceService,
    private readonly importService: AttendanceImportService,
    private readonly logService: OperationLogsService,
    private readonly authz: AuthorizationService,
  ) {}

  private getTodayDateOnly(): string {
    const today = new Date();
    const year = today.getFullYear();
    const month = String(today.getMonth() + 1).padStart(2, '0');
    const day = String(today.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }

  private canManageAllAttendance(req: { user: RequestUser }): boolean {
    return (
      this.authz.can(req, CaslAction.Manage, SubjectName.Attendance) ||
      // Legacy: class:edit grants broad attendance access for teacher scoping
      this.authz.can(req, CaslAction.Update, SubjectName.Class)
    );
  }

  private getAccessibleClassIds(req: { user: RequestUser }) {
    return this.service.getAccessibleClassIds(req.user.id, this.canManageAllAttendance(req));
  }

  private assertClassAccess(req: { user: RequestUser }, classId: number) {
    return this.service.assertClassAccess(req.user.id, classId, this.canManageAllAttendance(req));
  }


  @Get('attendance-period-configs')
  @RequirePermission('attendance:view')
  getAttendancePeriodConfigs() {
    return this.service.getAttendancePeriodConfigs();
  }

  @Put('attendance-period-configs')
  @RequirePermission('attendance:edit')
  async saveAttendancePeriodConfigs(
    @Body() dto: SaveAttendancePeriodConfigsDto,
    @Request() req: { user: RequestUser },
  ) {
    const result = await this.service.saveAttendancePeriodConfigs(dto);
    await this.logService.log({
      userId: req.user.id,
      username: req.user.username,
      module: '考勤管理',
      action: '保存考勤时段配置',
      targetType: 'attendancePeriodConfig',
      detail: dto.periods.map((item) => `${item.label}:${item.startTime}-${item.endTime}`).join('；'),
    });
    return result;
  }

  @Post('attendance-period-configs/reset')
  @RequirePermission('attendance:edit')
  async resetAttendancePeriodConfigs(@Request() req: { user: RequestUser }) {
    const result = await this.service.resetAttendancePeriodConfigs();
    await this.logService.log({
      userId: req.user.id,
      username: req.user.username,
      module: '考勤管理',
      action: '重置考勤时段配置',
      targetType: 'attendancePeriodConfig',
    });
    return result;
  }

  @Get('attendance-lessons/schedules/:scheduleId')
  @RequirePermission('attendance:view')
  async getLessonAttendance(
    @Param('scheduleId', ParseIntPipe) scheduleId: number,
    @Query() query: LessonAttendanceQueryDto,
    @Request() req: { user: RequestUser },
  ) {
    const result = await this.service.getLessonAttendance(scheduleId, query.date);
    await this.assertClassAccess(req, result.schedule.classId!);
    return result;
  }

  @Post('attendance-lessons/schedules/:scheduleId/pull')
  @RequirePermission('attendance:create')
  async pullLessonAttendance(
    @Param('scheduleId', ParseIntPipe) scheduleId: number,
    @Body() dto: StartLessonAttendanceDto,
    @Request() req: { user: RequestUser },
  ) {
    const schedule = await this.service.getLessonAttendance(scheduleId, dto.date);
    await this.assertClassAccess(req, schedule.schedule.classId!);
    const importClassIds = await this.service.getTeacherClassDingUserIds(
      req.user.id,
      schedule.schedule.classId!,
      this.canManageAllAttendance(req),
      dto.date,
    );
    const importRange = this.service.getLessonAttendanceImportDateRange(
      schedule.schedule,
      dto.date,
    );
    const importResult = await this.importService.importFromDingTalk({
      ...importRange,
      userIds: importClassIds,
      autoMatch: true,
      userId: req.user.id,
    });
    if (!importResult.success || importResult.errors.length > 0) {
      throw new BadRequestException(importResult.errors.join('; ') || '钉钉考勤拉取失败');
    }
    const result = await this.service.createLessonAttendanceFromDingTalk(
      scheduleId,
      dto.date,
      req.user.id,
    );
    await this.logService.log({
      userId: req.user.id,
      username: req.user.username,
      module: '考勤管理',
      action: schedule.session ? '查看已拉取课程考勤' : '拉取钉钉课程考勤',
      targetId: result.session.id,
      targetType: 'attendanceSession',
      detail: `排课${scheduleId} 日期${dto.date}，钉钉新增${importResult.imported}条，匹配${importResult.matched}条`,
    });
    return result;
  }

  @Post('attendance-lessons/:sessionId/complete')
  @RequirePermission('attendance:create')
  async completeLessonAttendance(
    @Param('sessionId', ParseIntPipe) sessionId: number,
    @Request() req: { user: RequestUser },
  ) {
    const session = await this.service.findAttendanceSession(sessionId);
    await this.assertClassAccess(req, session.classId);
    const result = await this.service.completeLessonAttendance(sessionId, req.user.id);
    await this.logService.log({
      userId: req.user.id,
      username: req.user.username,
      module: '考勤管理',
      action: '完成课程点名',
      targetId: sessionId,
      targetType: 'attendanceSession',
      detail: `班级${session.classId} 日期${session.lessonDate}`,
    });
    return result;
  }


  @Get('attendance-records/dingtalk-sync-status')
  @RequirePermission('attendance:view')
  async getDingTalkSyncStatus() {
    const latest = await this.logService.findLatestDingTalkAttendancePull();
    return {
      lastPulledAt: latest?.createdAt ?? null,
      action: latest?.action ?? null,
      username: latest?.username ?? null,
      detail: latest?.detail ?? null,
    };
  }

  @Post('attendance-records/refresh-dingtalk')
  @RequirePermission('attendance:create')
  async refreshDingTalkAttendance(
    @Body() dto: RefreshDingTalkAttendanceDto,
    @Request() req: { user: RequestUser },
  ) {
    if (dto.date > this.getTodayDateOnly()) {
      throw new BadRequestException('不能查看或刷新未来日期的考勤');
    }
    if (dto.classId) await this.assertClassAccess(req, dto.classId);
    const schedules = await this.service.getRefreshableSchedules(
      dto.date,
      dto.classId,
      dto.session,
      await this.getAccessibleClassIds(req),
    );
    let refreshed = 0;
    let imported = 0;
    let matched = 0;
    const errors: string[] = [];

    for (const schedule of schedules) {
      try {
        const importClassIds = await this.service.getTeacherClassDingUserIds(
          req.user.id,
          schedule.classId!,
          this.canManageAllAttendance(req),
          dto.date,
        );
        const importRange = this.service.getLessonAttendanceImportDateRange(schedule, dto.date);
        const importResult = await this.importService.importFromDingTalk({
          ...importRange,
          userIds: importClassIds,
          autoMatch: true,
          userId: req.user.id,
        });
        if (!importResult.success || importResult.errors.length > 0) {
          errors.push(...importResult.errors);
          continue;
        }
        await this.service.createLessonAttendanceFromDingTalk(schedule.id, dto.date, req.user.id);
        refreshed += 1;
        imported += importResult.imported;
        matched += importResult.matched;
      } catch (error: unknown) {
        errors.push((error as { message?: string })?.message || `排课 ${schedule.id} 刷新失败`);
      }
    }

    await this.logService.log({
      userId: req.user.id,
      username: req.user.username,
      module: '考勤管理',
      action: '刷新钉钉考勤',
      targetType: 'attendanceRecord',
      detail: `日期${dto.date}，排课${schedules.length}节，刷新${refreshed}节，钉钉新增${imported}条，匹配${matched}条${errors.length ? `，错误${errors.length}条` : ''}`,
      status: errors.length > 0 && refreshed === 0 ? 'failure' : 'success',
    });

    if (schedules.length === 0) {
      return { refreshed, imported, matched, errors: ['当前条件下没有可刷新的课程'] };
    }
    return { refreshed, imported, matched, errors };
  }

  // ── Batch create attendance records ──
  @Post('attendance-records/batch')
  @RequirePermission('attendance:create')
  async batchCreate(@Body() dto: BatchCreateAttendanceDto, @Request() req: any) {
    const { ipAddress, userAgent } = extractRequestInfo(req);
    const canManageAll = this.canManageAllAttendance(req);
    if (!canManageAll && dto.records.some((record) => record.classId == null)) {
      throw new ForbiddenException('教师录入考勤时必须关联自己任教的班级');
    }
    const classIds = [
      ...new Set(
        dto.records.map((record) => record.classId).filter((id): id is number => id != null),
      ),
    ];
    for (const classId of classIds) {
      await this.service.assertClassAccess(req.user.id, classId, canManageAll);
    }
    const result = await this.service.batchCreate(dto);
    await this.logService.log({
      userId: req.user?.id,
      username: req.user?.username,
      module: '考勤管理',
      action: '批量录入考勤',
      detail: `共 ${result.count} 条`,
      ipAddress,
      userAgent,
    });
    return result;
  }

  // ── Generate attendance records from schedules (with optional date range) ──
  @Post('attendance-records/generate-from-schedules')
  @RequirePermission('attendance:create')
  async generateFromSchedules(@Body() dto: GenerateFromSchedulesDto, @Request() req: any) {
    const { ipAddress, userAgent } = extractRequestInfo(req);
    await this.assertClassAccess(req, dto.classId);
    const result = await this.service.generateFromSchedules(dto);
    await this.logService.log({
      userId: req.user?.id,
      username: req.user?.username,
      module: '考勤管理',
      action: '按课表生成考勤',
      detail: `班级 ${dto.classId}, 共 ${result.count} 条`,
      ipAddress,
      userAgent,
    });
    return result;
  }

  // ── Export attendance records ──
  @Get('attendance-records/export')
  @RequirePermission('attendance:export')
  async exportRecords(
    @Query() query: QueryAttendanceRecordsDto,
    @Res() res: Response,
    @Request() req: { user: RequestUser },
  ) {
    if (query.classId) await this.assertClassAccess(req, query.classId);
    const classIds = await this.getAccessibleClassIds(req);
    const records = await this.service.findAllForExport(query, classIds);

    const workbook = new ExcelJS.Workbook();
    const ws = workbook.addWorksheet('考勤统计报表');
    ws.columns = [
      { header: '姓名', key: 'studentName', width: 15 },
      { header: '班级', key: 'className', width: 20 },
      { header: '日期', key: 'attendanceDate', width: 15 },
      { header: '时段', key: 'session', width: 15 },
      { header: '状态', key: 'status', width: 10 },
      { header: '来源', key: 'source', width: 10 },
      { header: '打卡设备', key: 'punchDevice', width: 30 },
      { header: '打卡时间', key: 'punchTime', width: 20 },
      { header: '备注', key: 'remark', width: 30 },
      { header: '归档时间', key: 'createdAt', width: 20 },
    ];
    ws.getRow(1).font = { bold: true };
    ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };

    for (const record of records) {
      ws.addRow({
        studentName: record.student?.name || '',
        className: record.class?.name || '',
        attendanceDate: record.attendanceDate || '',
        session: record.session || '',
        status: record.status || '',
        source: record.source || '',
        punchDevice: record.punchDeviceName || record.punchDeviceId || '',
        punchTime: record.punchTime
          ? record.punchTime.toISOString().replace('T', ' ').substring(0, 19)
          : '',
        remark: record.remark || '',
        createdAt: record.createdAt
          ? record.createdAt.toISOString().replace('T', ' ').substring(0, 19)
          : '',
      });
    }

    const dateRange = [query.dateFrom, query.dateTo].filter(Boolean).join('-') || '全部';

    res.setHeader(
      'Content-Type',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    );
    res.setHeader(
      'Content-Disposition',
      `attachment; filename=${encodeURIComponent(`考勤统计报表-${dateRange}`)}.xlsx`,
    );
    await workbook.xlsx.write(res);
    res.end();
  }

  @Get('attendance-records/schedules')
  @RequirePermission('attendance:view')
  async getAttendanceScheduleOptions(
    @Query() query: AttendanceScheduleOptionsQueryDto,
    @Request() req: { user: RequestUser },
  ) {
    await this.assertClassAccess(req, query.classId);
    return this.service.getScheduleOptionsForAttendance(query.classId, query.date);
  }

  // ── List attendance records with filters ──
  @Get('attendance-records')
  @RequirePermission('attendance:view')
  async findAll(@Query() query: QueryAttendanceRecordsDto, @Request() req: { user: RequestUser }) {
    if (query.classId) await this.assertClassAccess(req, query.classId);
    return this.service.findAll(query, await this.getAccessibleClassIds(req));
  }

  // ── Update a single attendance record ──
  @Put('attendance-records/:id')
  @RequirePermission('attendance:edit', 'attendance:self-edit')
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: UpdateAttendanceRecordDto,
    @Request() req: any,
  ) {
    const { ipAddress, userAgent } = extractRequestInfo(req);
    const existing = await this.service.findAttendanceRecord(id);
    if (existing.classId == null && !this.canManageAllAttendance(req)) {
      throw new ForbiddenException('无权修改未关联班级的考勤记录');
    }
    if (existing.classId != null) await this.assertClassAccess(req, existing.classId);
    const result = await this.service.update(id, dto);
    await this.logService.log({
      userId: req.user?.id,
      username: req.user?.username,
      module: '考勤管理',
      action: '编辑考勤记录',
      targetId: id,
      targetType: 'attendanceRecord',
      detail: `状态=${result.status}, 备注=${result.remark || ''}`,
      ipAddress,
      userAgent,
    });
    return result;
  }

  // ── Delete a single attendance record ──
  @Delete('attendance-records/:id')
  @RequirePermission('attendance:edit', 'attendance:self-edit')
  async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
    const { ipAddress, userAgent } = extractRequestInfo(req);
    const existing = await this.service.findAttendanceRecord(id);
    if (existing.classId == null && !this.canManageAllAttendance(req)) {
      throw new ForbiddenException('无权删除未关联班级的考勤记录');
    }
    if (existing.classId != null) await this.assertClassAccess(req, existing.classId);
    const result = await this.service.remove(id);
    await this.logService.log({
      userId: req.user?.id,
      username: req.user?.username,
      module: '考勤管理',
      action: '归档考勤记录',
      targetId: id,
      targetType: 'attendanceRecord',
      detail: `归档考勤记录 ${id}`,
      ipAddress,
      userAgent,
    });
    return result;
  }

  // ── Get distinct classes with attendance records ──
  @Get('attendance-records/classes')
  @RequirePermission('attendance:view')
  async getClasses(@Request() req: { user: RequestUser }) {
    return this.service.getClasses(await this.getAccessibleClassIds(req));
  }

  // ── Attendance summary ──
  @Get('attendance-records/summary')
  @RequirePermission('attendance:view')
  async getSummary(
    @Query() query: AttendanceSummaryQueryDto,
    @Request() req: { user: RequestUser },
  ) {
    if (query.classId) await this.assertClassAccess(req, query.classId);
    return this.service.getSummary(query, await this.getAccessibleClassIds(req));
  }

  // ── Attendance calendar ──
  @Get('attendance-records/calendar')
  @RequirePermission('attendance:view')
  async getCalendar(
    @Query() query: AttendanceCalendarQueryDto,
    @Request() req: { user: RequestUser },
  ) {
    await this.assertClassAccess(req, query.classId);
    return this.service.getCalendar(query);
  }

  // ── DingAttendance raw records ──
  @Get('ding-attendance-raw')
  @RequirePermission('attendance:view')
  async getDingRaw(@Query() query: QueryDingRawDto, @Request() req: { user: RequestUser }) {
    if (query.classId) await this.assertClassAccess(req, query.classId);
    return this.service.getDingRaw(query, await this.getAccessibleClassIds(req));
  }

  // ── Match a dingtalk record to a student ──
  @Post('ding-attendance-raw/:id/match')
  @RequirePermission('attendance:edit')
  async matchDingRecord(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: MatchDingRecordDto,
    @Request() req: any,
  ) {
    const { ipAddress, userAgent } = extractRequestInfo(req);
    const result = await this.service.matchDingRecord(id, dto);
    await this.logService.log({
      userId: req.user?.id,
      username: req.user?.username,
      module: '考勤管理',
      action: '匹配考勤记录',
      targetId: id,
      targetType: 'dingAttendanceRaw',
      detail: `匹配到学生 ${dto.studentId}`,
      ipAddress,
      userAgent,
    });
    return result;
  }

  // ── Attendance class-based report export ──
  @Get('attendance-records/report')
  @RequirePermission('attendance:export')
  async exportReport(
    @Query() query: AttendanceReportQueryDto,
    @Res() res: Response,
    @Request() req: any,
  ) {
    if (query.classId) await this.assertClassAccess(req, query.classId);
    const reportData = await this.service.getReport(query, await this.getAccessibleClassIds(req));

    const workbook = new ExcelJS.Workbook();
    const ws = workbook.addWorksheet('考勤统计报表');
    ws.columns = [
      { header: '班级名称', key: 'className', width: 30 },
      { header: '总记录数', key: 'total', width: 12 },
      { header: '出勤', key: 'present', width: 10 },
      { header: '出勤率', key: 'presentRate', width: 10 },
      { header: '缺勤', key: 'absent', width: 10 },
      { header: '缺勤率', key: 'absentRate', width: 10 },
      { header: '迟到', key: 'late', width: 10 },
      { header: '迟到率', key: 'lateRate', width: 10 },
      { header: '请假', key: 'leave', width: 10 },
      { header: '请假率', key: 'leaveRate', width: 10 },
    ];
    ws.getRow(1).font = { bold: true };
    ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };

    for (const row of reportData) {
      ws.addRow({
        className: row.className,
        total: row.total,
        present: row.present,
        presentRate: `${row.presentRate}%`,
        absent: row.absent,
        absentRate: `${row.absentRate}%`,
        late: row.late,
        lateRate: `${row.lateRate}%`,
        leave: row.leave,
        leaveRate: `${row.leaveRate}%`,
      });
    }

    // Audit log
    const { ipAddress, userAgent } = extractRequestInfo(req);
    await this.logService.log({
      userId: req.user?.id,
      username: req.user?.username,
      module: '考勤管理',
      action: '导出考勤报表',
      detail: `classId=${query.classId || '全部'} ${query.dateFrom || ''}~${query.dateTo || ''}`,
      ipAddress,
      userAgent,
    });

    res.setHeader(
      'Content-Type',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    );
    res.setHeader('Content-Disposition', 'attachment; filename=attendance-report.xlsx');
    await workbook.xlsx.write(res);
    res.end();
  }

  // ── Abnormal attendance alerts ──
  @Get('attendance-records/alerts')
  @RequirePermission('attendance:view')
  async getAlerts(
    @Request() req: { user: RequestUser },
    @Query() query: AttendanceAlertsQueryDto,
  ) {
    return this.service.getAlerts(
      query.days ?? 14,
      query.threshold ?? 3,
      await this.getAccessibleClassIds(req),
    );
  }

  @Post('ding-attendance-raw/auto-match')
  @RequirePermission('attendance:edit')
  async autoMatch() {
    return this.service.autoMatchDingRecords();
  }

  // ═══════════════════════════════════════════════════════════════
  // DingTalk attendance import with SSE streaming progress
  // ═══════════════════════════════════════════════════════════════

  @Get('attendance-records/import/dingtalk/classes')
  @RequirePermission('attendance:create')
  getDingTalkImportClasses(@Request() req: { user: RequestUser }) {
    return this.service.getImportableClasses(req.user.id, this.canManageAllAttendance(req));
  }

  /**
   * Trigger DingTalk attendance import.
   * Mirrors `dws attendance check result` pipeline:
   *   fetch → parse → deduplicate → save → auto-match.
   */
  @Post('attendance-records/import/dingtalk')
  @RequirePermission('attendance:create')
  async importFromDingTalk(@Body() dto: DingTalkImportDto, @Request() req: { user: RequestUser }) {
    const { ipAddress, userAgent } = extractRequestInfo(req);
    const canManageAll = this.canManageAllAttendance(req);
    let userIds: string[];

    if (dto.users) {
      if (!canManageAll) {
        throw new ForbiddenException('仅管理员可指定钉钉用户范围');
      }
      userIds = dto.users
        .split(',')
        .map((value) => value.trim())
        .filter(Boolean);
    } else {
      if (!dto.classId) {
        throw new BadRequestException('请选择要拉取考勤的班级');
      }
      userIds = await this.service.getTeacherClassDingUserIds(
        req.user.id,
        dto.classId,
        canManageAll,
        dto.start,
      );
    }

    const startDate = dto.start ?? this.getTodayDateOnly();
    const endDate = dto.end ?? startDate;
    const result = await this.importService.importFromDingTalk({
      startDate,
      endDate,
      userIds,
      autoMatch: true,
      userId: req.user.id,
    });

    await this.logService.log({
      userId: req.user?.id,
      username: req.user?.username,
      module: '考勤管理',
      action: '钉钉考勤导入',
      detail: `${startDate}~${endDate}, 导入=${result.imported}, 跳过=${result.skipped}, 匹配=${result.matched}`,
      ipAddress,
      userAgent,
    });

    return result;
  }

  /**
   * SSE stream for live import progress.
   * Connect before triggering the import to receive real-time progress events.
   *
   * NOTE: @RequirePermission works with @Sse() in NestJS because guards
   * execute in the standard request pipeline before the SSE handler is invoked.
   * If this ever breaks after a NestJS upgrade, verify guard execution order.
   */
  @Sse('attendance-records/import/dingtalk/stream')
  @RequirePermission('attendance:view')
  importProgressStream(@Request() req: { user: RequestUser }): Observable<SseEvent> {
    const userId = req.user.id;
    return new Observable<SseEvent>((subscriber) => {
      const subscription = this.importService.progress$
        .pipe(
          filter((event) => event.userId === userId),
        )
        .subscribe({
          next: (event) => {
            subscriber.next({ data: JSON.stringify(event) });
            if (event.phase === 'complete' || event.phase === 'error') {
              subscriber.complete();
            }
          },
          error: (err: unknown) => subscriber.error(err),
        });
      return () => subscription.unsubscribe();
    });
  }
}
