import { Injectable } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { MeetingRepository } from '../repositories/meeting.repository';
import { AttendanceRepository } from '../repositories/attendance.repository';
import { RequiredAttendeeRepository } from '../repositories/required-attendee.repository';
import { MeetingAttendanceError } from '../errors/meeting-attendance.error';
import { canCheckIn, deriveMeetingStatus, isLate, normalizeName } from '../utils/meeting-utils';
import {
  MEETING_ATTENDANCE_AUDIT_ACTIONS,
  MEETING_ATTENDANCE_AUDIT_RESOURCES,
} from '../constants/audit';
import { MeetingAttendanceAuditLogWriter } from './audit-log-writer.service';
import type { Request } from 'express';

/**
 * v1.2 签到方式校验三层 fallback 结果
 * source 解释：
 * - MEETING_OVERRIDE：会议级临时调整（MeetingRequiredAttendee.checkinMode）
 * - SERIES_PREFERENCE：系列级参会人默认覆盖（MeetingSeriesAttendeePreference）
 * - CITY_DERIVED：按 user.workCity vs meeting.city 派生
 */
export type AllowedCheckinMode = {
  mode: 'ON_SITE' | 'ONLINE' | null;
  source: 'MEETING_OVERRIDE' | 'SERIES_PREFERENCE' | 'CITY_DERIVED';
};

@Injectable()
export class CheckinService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly meetingRepository: MeetingRepository,
    private readonly attendanceRepository: AttendanceRepository,
    private readonly requiredAttendeeRepository: RequiredAttendeeRepository,
    private readonly auditLogWriter: MeetingAttendanceAuditLogWriter,
  ) {}

  async checkin(
    meetingId: string,
    payload: {
      qrData?: string;
      qrType?: string;
      checkinType?: string;
      attendanceStatus?: string;
      deviceId?: string;
    },
    userId: string,
  ) {
    const meeting = (await this.prisma.meeting.findUnique({
      where: { id: meetingId },
      select: {
        id: true,
        title: true,
        status: true,
        startTime: true,
        endTime: true,
        seriesId: true,
        city: true,
        enforceCheckinMode: true,
      } as any,
    })) as any;
    if (!meeting) {
      throw new MeetingAttendanceError(404, 'Meeting not found');
    }

    if (!['SCHEDULED', 'IN_PROGRESS'].includes(meeting.status)) {
      throw new MeetingAttendanceError(400, 'Meeting has not started or has ended');
    }

    const checkinType = payload.checkinType || 'QR_CODE';

    if (checkinType === 'QR_CODE' && payload.qrData) {
      try {
        const parsedQRData = JSON.parse(payload.qrData);
        if (!parsedQRData.meetingId || !parsedQRData.type) {
          throw new MeetingAttendanceError(400, 'Invalid QR code');
        }
      } catch {
        throw new MeetingAttendanceError(400, 'Invalid QR code format');
      }
    }

    const now = new Date();
    if (!canCheckIn(meeting.startTime, now)) {
      throw new MeetingAttendanceError(
        400,
        'Check-in is not yet available. You can check in 15 minutes before the meeting starts.',
      );
    }

    const userIsLate = isLate(meeting.startTime, now);

    // v1.2 签到方式校验（登录用户版）
    const qrType = this.normalizeQrType(payload);
    if (meeting.enforceCheckinMode && checkinType === 'QR_CODE') {
      const user = await this.prisma.user.findUnique({
        where: { id: userId },
        select: { id: true, workCity: true } as any,
      });
      if (!user) {
        throw new MeetingAttendanceError(404, 'User not found');
      }
      const requiredAttendeeFull = await (this.prisma as any).meetingRequiredAttendee.findUnique({
        where: { meetingId_userId: { meetingId, userId } },
        select: { userId: true, checkinMode: true },
      });
      if (!requiredAttendeeFull) {
        throw new MeetingAttendanceError(
          403,
          'You are not on the attendee list for this meeting and cannot check in',
        );
      }
      const allowed = await this.resolveAllowedCheckinMode(meeting, requiredAttendeeFull, user as any);
      if (allowed.mode === null) {
        throw new MeetingAttendanceError(
          403,
          '请联系管理员配置您的工作地',
          'MEETING_ATTENDANCE_034',
        );
      }
      if (allowed.mode !== qrType) {
        const expectedLabel = allowed.mode === 'ON_SITE' ? '线下' : '线上';
        throw new MeetingAttendanceError(
          400,
          `请使用${expectedLabel}专属二维码`,
          'MEETING_ATTENDANCE_033',
        );
      }
    }

    if (payload.deviceId) {
      const currentUserDeviceCheckin = await this.prisma.meetingAttendance.findFirst({
        where: {
          meetingId,
          deviceId: payload.deviceId,
          userId,
        },
      });

      if (!currentUserDeviceCheckin) {
        const otherUserDeviceCheckin = await this.attendanceRepository.findAttendanceByDeviceOtherUser(
          meetingId,
          payload.deviceId,
          userId,
        );

        if (otherUserDeviceCheckin) {
          throw new MeetingAttendanceError(
            400,
            'This device has already been used for check-in by another user. Each device can only be used for one check-in per meeting.',
          );
        }
      }
    }

    const existingAttendance = await this.attendanceRepository.findAttendanceByUserMeeting(userId, meetingId);
    if (existingAttendance && ['ON_SITE', 'ONLINE', 'LATE'].includes(existingAttendance.status)) {
      throw new MeetingAttendanceError(
        400,
        `You have already checked in as ${existingAttendance.status}. Cannot check in again.`,
      );
    }

    // v1.2 status 防绕过：QR 码扫码时 status 按 qrType 派生；非 QR 码（MANUAL）保持前端传值兼容
    let finalStatus: string;
    if (checkinType === 'QR_CODE') {
      finalStatus = qrType;
    } else {
      finalStatus = payload.attendanceStatus || 'ONLINE';
    }
    if (userIsLate) {
      finalStatus = 'LATE';
    }

    const statusLabel = finalStatus === 'ONLINE' ? 'Online' : finalStatus === 'ON_SITE' ? 'On-site' : finalStatus;
    const notes = userIsLate
      ? `Checked in late via ${checkinType === 'QR_CODE' ? 'QR code' : 'manual'} - ${statusLabel}`
      : `Checked in on time via ${checkinType === 'QR_CODE' ? 'QR code' : 'manual'} - ${statusLabel}`;

    const attendance = existingAttendance
      ? await this.attendanceRepository.updateAttendance(existingAttendance.id, {
          status: finalStatus as any,
          checkinTime: now,
          isLate: userIsLate,
          checkinType: checkinType as any,
          notes,
          deviceId: payload.deviceId || null,
        })
      : await this.attendanceRepository.createAttendance({
          user: { connect: { id: userId } },
          meeting: { connect: { id: meetingId } },
          status: finalStatus as any,
          checkinTime: now,
          isLate: userIsLate,
          checkinType: checkinType as any,
          notes,
          deviceId: payload.deviceId || null,
        });

    return {
      message: 'Check-in successful',
      attendance,
      isLate: userIsLate,
      debug: {
        deviceId: payload.deviceId,
        deviceIdType: typeof payload.deviceId,
        attendanceDeviceId: attendance.deviceId,
      },
    };
  }

  /**
   * v1.2 签到方式三层 fallback 求值：
   * 1) MeetingRequiredAttendee.checkinMode（会议级临时调整）
   * 2) MeetingSeriesAttendeePreference.defaultCheckinMode（系列级参会人覆盖）
   * 3) user.workCity vs meeting.city 派生（trim 后区分大小写精确比较）
   *
   * 传入 requiredAttendee 已 include `checkinMode` 字段；meeting 已 include `city / enforceCheckinMode / seriesId`；
   * user 只需 `workCity`。
   */
  async resolveAllowedCheckinMode(
    meeting: { id: string; city?: string | null; seriesId?: string | null },
    requiredAttendee: { userId: string; checkinMode?: 'ON_SITE' | 'ONLINE' | null },
    user: { id: string; workCity?: string | null },
  ): Promise<AllowedCheckinMode> {
    // Layer 1: meeting-level override
    if (requiredAttendee.checkinMode) {
      return { mode: requiredAttendee.checkinMode, source: 'MEETING_OVERRIDE' };
    }

    // Layer 2: series-level preference
    if (meeting.seriesId) {
      const seriesPref = await (this.prisma as any).meetingSeriesAttendeePreference.findUnique({
        where: { seriesId_userId: { seriesId: meeting.seriesId, userId: requiredAttendee.userId } },
        select: { defaultCheckinMode: true },
      });
      if (seriesPref?.defaultCheckinMode) {
        return { mode: seriesPref.defaultCheckinMode, source: 'SERIES_PREFERENCE' };
      }
    }

    // Layer 3: city-derived
    const userCity = user.workCity?.trim() || null;
    const meetingCity = meeting.city?.trim() || null;
    if (!userCity) {
      return { mode: null, source: 'CITY_DERIVED' };
    }
    return {
      mode: userCity === meetingCity ? 'ON_SITE' : 'ONLINE',
      source: 'CITY_DERIVED',
    };
  }

  /**
   * v1.2 归一化 qrType（URL 参数 type=on_site|online 或 legacy attendanceStatus=ON_SITE|ONLINE）
   * 防绕过：实际签到 status 只根据 qrType 写入，忽略前端 attendanceStatus。
   */
  private normalizeQrType(
    payload: { qrType?: string; attendanceStatus?: string },
  ): 'ON_SITE' | 'ONLINE' {
    const raw = (payload.qrType ?? payload.attendanceStatus ?? 'on_site').toString().toLowerCase();
    if (raw === 'online' || raw === 'online_code') return 'ONLINE';
    return 'ON_SITE';
  }

  async guestCheckin(
    meetingId: string,
    payload: {
      name: string;
      email?: string;
      attendanceStatus?: string;
      qrType?: string;
      deviceId?: string;
    },
  ) {
    const meeting = (await this.prisma.meeting.findUnique({
      where: { id: meetingId },
      select: {
        id: true,
        title: true,
        status: true,
        startTime: true,
        endTime: true,
        seriesId: true,
        city: true,
        enforceCheckinMode: true,
      } as any,
    })) as any;
    if (!meeting) {
      throw new MeetingAttendanceError(404, 'Meeting not found');
    }

    if (!payload.name || !payload.name.trim()) {
      throw new MeetingAttendanceError(400, 'Name cannot be empty');
    }

    const now = new Date();
    const derivedStatus = deriveMeetingStatus(meeting.status, meeting.startTime, meeting.endTime, now);
    if (derivedStatus !== meeting.status && meeting.status !== 'CANCELLED') {
      await this.meetingRepository.updateMeetingStatus(meetingId, derivedStatus).catch(() => undefined);
    }

    if (derivedStatus === 'COMPLETED') {
      throw new MeetingAttendanceError(400, 'The meeting has ended, check-in is not allowed');
    }

    if (derivedStatus === 'CANCELLED') {
      throw new MeetingAttendanceError(400, 'The meeting has been cancelled');
    }

    if (now > meeting.endTime) {
      throw new MeetingAttendanceError(400, 'The meeting has ended. Check-in is closed.');
    }

    if (!canCheckIn(meeting.startTime, now)) {
      throw new MeetingAttendanceError(
        400,
        'Check-in is not yet available. You can check in 15 minutes before the meeting starts.',
      );
    }

    // v1.2 Step A: 提前匹配参会人（为签到方式校验服务）
    const allAttendees = await this.requiredAttendeeRepository.listRequiredAttendeesWithUsers(meetingId);
    const normalizedInputName = normalizeName(payload.name);
    const normalizedEmail = payload.email?.trim().toLowerCase();
    const matchedAttendee = allAttendees.find((attendee) => {
      const attendeeEmail = attendee.user?.email?.toLowerCase();
      const attendeeName = attendee.user?.displayName;

      if (normalizedEmail && attendeeEmail === normalizedEmail) {
        return true;
      }

      if (attendeeName) {
        return normalizeName(attendeeName) === normalizedInputName;
      }

      return false;
    });

    if (!matchedAttendee?.user) {
      throw new MeetingAttendanceError(
        403,
        'You are not on the attendee list for this meeting and cannot check in',
      );
    }

    // v1.2 Step B: 签到方式校验（当会议开启 enforceCheckinMode 时）
    const qrType = this.normalizeQrType(payload);
    if (meeting.enforceCheckinMode) {
      const user = await this.prisma.user.findUnique({
        where: { id: matchedAttendee.user.id },
        select: { id: true, workCity: true } as any,
      });
      const requiredAttendeeFull = await (this.prisma as any).meetingRequiredAttendee.findUnique({
        where: { meetingId_userId: { meetingId, userId: matchedAttendee.user.id } },
        select: { userId: true, checkinMode: true },
      });

      const allowed = await this.resolveAllowedCheckinMode(
        meeting,
        requiredAttendeeFull ?? { userId: matchedAttendee.user.id, checkinMode: null },
        user as any,
      );

      if (allowed.mode === null) {
        throw new MeetingAttendanceError(
          403,
          '请联系管理员配置您的工作地',
          'MEETING_ATTENDANCE_034',
        );
      }
      if (allowed.mode !== qrType) {
        const expectedLabel = allowed.mode === 'ON_SITE' ? '线下' : '线上';
        throw new MeetingAttendanceError(
          400,
          `请使用${expectedLabel}专属二维码`,
          'MEETING_ATTENDANCE_033',
        );
      }
    }

    const userIsLate = isLate(meeting.startTime, now);
    // v1.2 防绕过：status 按 qrType 派生，忽略 Body 传入的 attendanceStatus
    let finalStatus: string = qrType;
    if (userIsLate) {
      finalStatus = 'LATE';
    }

    const notes = userIsLate
      ? `late - ${qrType === 'ON_SITE' ? 'On-site' : 'Online'}`
      : `on time - ${qrType === 'ON_SITE' ? 'On-site' : 'Online'}`;

    if (payload.deviceId) {
      const existingDeviceCheckin = await this.attendanceRepository.findAttendanceByDevice(meetingId, payload.deviceId);
      if (existingDeviceCheckin) {
        throw new MeetingAttendanceError(
          400,
          'This device has already been used for check-in. Each device can only be used for one check-in per meeting.',
        );
      }
    }

    const existingAttendance = await this.attendanceRepository.findAttendanceByUserMeeting(
      matchedAttendee.user.id,
      meetingId,
    );
    if (existingAttendance && ['ON_SITE', 'ONLINE', 'LATE'].includes(existingAttendance.status)) {
      throw new MeetingAttendanceError(
        400,
        `You have already checked in as ${existingAttendance.status}. Cannot check in again.`,
      );
    }

    const attendance = await this.attendanceRepository.upsertAttendance(
      matchedAttendee.user.id,
      meetingId,
      {
        status: finalStatus as any,
        checkinTime: now,
        isLate: userIsLate,
        checkinType: 'QR_CODE',
        notes,
        deviceId: payload.deviceId || null,
      },
      {
        user: { connect: { id: matchedAttendee.user.id } },
        meeting: { connect: { id: meetingId } },
        status: finalStatus as any,
        checkinTime: now,
        isLate: userIsLate,
        checkinType: 'QR_CODE',
        notes,
        deviceId: payload.deviceId || null,
      },
    );

    return {
      message: userIsLate
        ? 'Check-in successful! You are late, please be mindful of the time.'
        : 'Check-in successful!',
      attendance,
      isLate: userIsLate,
      debug: {
        deviceId: payload.deviceId,
        deviceIdType: typeof payload.deviceId,
        attendanceDeviceId: attendance.deviceId,
      },
    };
  }

  async updateAttendanceStatus(
    meetingId: string,
    userId: string,
    payload: { status: string; notes?: string },
    request?: Request,
    actor?: { userId?: string; id?: string; email?: string; displayName?: string; username?: string; roles?: any[] },
  ) {
    const validStatuses = [
      'ON_SITE',
      'ONLINE',
      'ABSENT',
      'PTO',
      'BUSINESS_CONFLICT',
      'LATE',
      'MEETING',
      'NOT_CHECKED_IN',
    ];

    if (!validStatuses.includes(payload.status)) {
      throw new MeetingAttendanceError(400, 'Invalid attendance status');
    }

    const meeting = await this.meetingRepository.findMeetingById(meetingId);
    if (!meeting) {
      throw new MeetingAttendanceError(404, 'Meeting not found');
    }

    const user = await this.prisma.user.findUnique({
      where: { id: userId, deletedAt: null },
      select: { id: true },
    });
    if (!user) {
      throw new MeetingAttendanceError(404, 'User not found');
    }

    let finalNotes = payload.notes;
    if (payload.status === 'NOT_CHECKED_IN' && !payload.notes) {
      finalNotes = 'Manually changed to not checked in';
    }

    const now = new Date();
    const existingAttendance = await this.attendanceRepository.findAttendanceByUserMeeting(userId, meetingId);

    const attendance = existingAttendance
      ? await this.attendanceRepository.updateAttendance(existingAttendance.id, {
          status: payload.status as any,
          notes: finalNotes,
          checkinTime: ['LATE', 'ON_SITE', 'ONLINE'].includes(payload.status)
            ? now
            : existingAttendance.checkinTime,
          isLate: payload.status === 'LATE',
        })
      : await this.attendanceRepository.createAttendance({
          user: { connect: { id: userId } },
          meeting: { connect: { id: meetingId } },
          status: payload.status as any,
          notes: finalNotes,
          checkinTime: ['LATE', 'ON_SITE', 'ONLINE'].includes(payload.status) ? now : null,
          isLate: payload.status === 'LATE',
        });

    await this.auditLogWriter.log({
      request,
      actor,
      action: existingAttendance
        ? MEETING_ATTENDANCE_AUDIT_ACTIONS.ATTENDANCE_UPDATE
        : MEETING_ATTENDANCE_AUDIT_ACTIONS.ATTENDANCE_MANUAL,
      resource: MEETING_ATTENDANCE_AUDIT_RESOURCES.ATTENDANCE,
      statusCode: 200,
      resourceId: attendance.id,
      requestBody: { status: payload.status, notes: payload.notes },
      changes: {
        action: existingAttendance ? 'update' : 'create',
        before: existingAttendance,
        after: attendance,
        userId: user.id,
        meetingId,
        meetingTitle: meeting.title,
      },
    });

    return attendance;
  }

}
