import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { formatInTimeZone, fromZonedTime } from 'date-fns-tz';
import { addMinutes } from 'date-fns/addMinutes';
import { isBefore } from 'date-fns/isBefore';
import {
  Between,
  IsNull,
  Not,
} from 'typeorm';
import {
  DatabaseService,
  envConfig,
  ErrorResponse,
  logger,
  MailService,
  UploadService,
} from './config';
import {
  AttendanceStatusEnum,
  AttendanceTypeEnum,
  NotificationTypeEnum,
  StatusEnum,
} from './config/database/entities';
import { IFile } from './config/types/interface';
import { normalizeTimezone } from './common/timezone/timezone.util';
import { holidayResponseDto } from './modules/holidays/holidays.dto';
import { NotificationsService } from './modules/notifications/notifications.service';
import {
  getCurrentMonthHolidays,
  getUtcStartEndOfDate,
} from './utils';

type WorkingSchedule = {
  timezone?: string;
  working_days: string[];
  working_days_per_week: number;
  working_hours_per_day: number;
  office_start_time: Date;
  office_end_time: Date;
  half_day_min_time: number;
  half_day_max_time: number;
};

@Injectable()
export class AppService {
  constructor(
    private readonly uploadService: UploadService,
    private readonly db: DatabaseService,
    private readonly mailService: MailService,
    private readonly notificationService: NotificationsService,
  ) {}

  async uploadFiles(
    files?: Express.Multer.File[],
    type?: string,
  ): Promise<IFile[] | IFile> {
    try {
      logger.info('get files ' + type);
      logger.info('get files ' + files);

      if (!files || !files.length) {
        throw new ErrorResponse('Files not found', 400);
      }
      const { host } = envConfig().server || {};
      const paths = await this.uploadService.saveImages(files, 'temp');
      if (type === 'single') {
        return {
          ...paths[0],
          file_name: `${host}/${paths[0].file_name}`,
        };
      }
      return paths?.map((file) => ({
        ...file,
        file_name: `${host}/${file.file_name}`,
      }));
    } catch (error) {
      logger.error(`file upload error: ${error}`);
      if (error instanceof ErrorResponse) {
        throw error;
      }
      throw new ErrorResponse(error, 400);
    }
  }

  // checkout reminder every 10 minutes
  @Cron('0 */10 * * * *')
  async handleReminderCorn() {
    try {
      logger.log('Checkout reminder cron started');
      const now = new Date();

      const owners = await this.db.owner.findMany({
        where: { status: StatusEnum.active },
        include: { workingSchedule: true },
      });

      for (const owner of owners) {
        try {
          const schedule = owner.workingSchedule;
          if (!schedule) continue;

          const timeZone = normalizeTimezone(schedule.timezone);
          const todayStr = formatInTimeZone(now, timeZone, 'yyyy-MM-dd');
          const { startUtc: todayStart, endUtc: todayEnd } =
            getUtcStartEndOfDate(todayStr, timeZone);

          const officeEnd = new Date(schedule.office_end_time);
          const officeHour = Number(
            formatInTimeZone(officeEnd, timeZone, 'HH'),
          );
          const officeMinute = Number(
            formatInTimeZone(officeEnd, timeZone, 'mm'),
          );
          const officeSecond = Number(
            formatInTimeZone(officeEnd, timeZone, 'ss'),
          );

          const officeEndToday = fromZonedTime(
            `${todayStr} ${String(officeHour).padStart(2, '0')}:${String(officeMinute).padStart(2, '0')}:${String(officeSecond).padStart(2, '0')}`,
            timeZone,
          );

          const flexibleReminderTime = fromZonedTime(
            `${todayStr} 23:30:00`,
            timeZone,
          );

          const attendances = await this.db.attendances.findMany({
            where: {
              owner_id: owner.id,
              date: Between(todayStart, todayEnd),
              check_in: Not(IsNull()),
              check_out: IsNull(),
              checkout_reminder_sent: false,
            },
            include: { member: true },
          });

          if (!attendances.length) continue;

          for (const attendance of attendances) {
            try {
              const member = attendance.member;
              if (!member) continue;

              let reminderTime: Date;
              switch (attendance.type) {
                case AttendanceTypeEnum.flexible:
                  reminderTime = flexibleReminderTime;
                  break;
                case AttendanceTypeEnum.in_office:
                default:
                  reminderTime = addMinutes(officeEndToday, -30);
                  break;
              }

              if (isBefore(now, reminderTime)) continue;

              await this.mailService.sendMail(
                'attendance-reminder-mail',
                member?.email,
                {
                  name: member?.first_name
                    ? `${member?.first_name} ${member?.last_name}`
                    : member.nickname,
                  checkInTime: attendance.check_in,
                  workedHours: attendance.work_hours,
                  dashboard:
                    'https://team-tracker.rinov8.studio/admin/dashboard',
                },
                'Checkout Reminder',
              );

              await this.db.attendances.update({
                where: { id: attendance.id },
                data: {
                  checkout_reminder_sent: true,
                  checkout_reminder_sent_at: new Date(),
                },
              });

              await this.notificationService.sendNotification({
                title: 'Checkout Reminder',
                message: `Hi ${member.first_name}, this is a reminder to check out for today. You checked in at ${formatInTimeZone(attendance.check_in, timeZone, 'hh:mm a')} and have currently completed ${attendance.work_hours} working hours. Please complete your checkout if your workday has ended.`,
                member_id: member.id,
                type: NotificationTypeEnum.attendance,
                owner_id: owner.id,
                include_owner: false,
                start_time: attendance?.check_in,
                duration: 0,
              });

              logger.log(
                `Reminder sent → ${member.email} (${attendance.type})`,
              );
            } catch (err: any) {
              logger.error(
                `Failed attendance reminder: ${attendance.id}`,
                err?.stack,
              );
            }
          }
        } catch (err: any) {
          logger.error(`Owner processing failed: ${owner.id}`, err?.stack);
        }
      }

      logger.log('Checkout reminder cron completed');
    } catch (err: any) {
      logger.error('Checkout reminder cron crashed', err?.stack);
    }
  }

  // ==================== DAILY ATTENDANCE CRON ====================
  @Cron('0 * * * *')
  async generateDailyAttendance() {
    logger.log('Attendance cron started');
    console.log('Attendance cron started at', new Date().toISOString());
    try {
      const now = new Date();

      const [
        members,
        owners,
        workDaySchedules,
        recurringHolidays,
        nonRecurringHolidays,
        approvedLeaves,
        existingAttendances,
        openPreviousAttendances,
      ] = await Promise.all([
        this.db.members.findMany({ where: { status: 'active' } }),

        this.db.owner.findMany({
          include: { workingSchedule: true },
        }),

        this.db.work_day_schedules.findMany({}),

        this.db.holidays.findMany({
          where: { is_recurring: true },
          include: { departments: true },
        }),

        this.db.holidays.findMany({
          where: { is_recurring: false },
          include: { departments: true },
        }),

        this.db.leaves.findMany({
          where: {
            status: 'approved',
          },
        }),

        this.db.attendances.findMany({
          select: { member_id: true, date: true, owner_id: true },
        }),

        this.db.attendances.findMany({
          where: {
            check_in: Not(IsNull()),
            check_out: IsNull(),
            status: Not(AttendanceStatusEnum.absent),
          },
          include: { member: true },
        }),
      ]);

      const ownerScheduleMap = new Map<string, WorkingSchedule | null>(
        owners.map((o) => [o.id, o.workingSchedule as WorkingSchedule | null]),
      );

      const ownerHolidayMap = new Map<string, holidayResponseDto[]>();

      for (const owner of owners) {
        const timeZone = normalizeTimezone(owner.workingSchedule?.timezone);
        const todayStr = formatInTimeZone(now, timeZone, 'yyyy-MM-dd');
        const { startUtc: ownerTodayStart } = getUtcStartEndOfDate(
          todayStr,
          timeZone,
        );

        const ownerRecurring = recurringHolidays.filter(
          (h) => h.owner_id === owner.id,
        );
        const ownerNonRecurring = nonRecurringHolidays.filter((h) => {
          if (h.owner_id !== owner.id) return false;
          return (
            new Date(h.start_date) <= ownerTodayStart &&
            new Date(h.end_date) >= ownerTodayStart
          );
        });

        const holidays = getCurrentMonthHolidays(
          ownerRecurring,
          ownerNonRecurring,
          ownerTodayStart,
          ownerTodayStart,
          timeZone,
        );
        ownerHolidayMap.set(owner.id, holidays);
      }

      // ====================== 1. UPDATE PREVIOUS DAY OPEN ATTENDANCES ======================
      for (const att of openPreviousAttendances) {
        try {
          if (!att.member) continue;

          const schedule = ownerScheduleMap.get(att.member.owner_id);
          const timeZone = normalizeTimezone(schedule?.timezone);
          const todayStr = formatInTimeZone(now, timeZone, 'yyyy-MM-dd');
          const { startUtc: memberTodayStart } = getUtcStartEndOfDate(
            todayStr,
            timeZone,
          );
          const attendanceDate = new Date(att.date);

          if (attendanceDate.getTime() >= memberTodayStart.getTime()) {
            continue;
          }

          await this.db.attendances.update({
            where: { id: att.id },
            data: {
              status: AttendanceStatusEnum.absent,
            },
          });

          logger.log(
            `Updated previous day attendance for member ${att.member_id}`,
          );
        } catch (err: any) {
          logger.error(
            `Failed to update previous attendance ${att.id}`,
            err?.stack,
          );
        }
      }

      // ====================== 2. CREATE TODAY'S ATTENDANCES ======================
      for (const member of members) {
        try {
          const ownerId = member.owner_id;
          const schedule = ownerScheduleMap.get(ownerId);
          const timeZone = normalizeTimezone(schedule?.timezone);
          const todayStr = formatInTimeZone(now, timeZone, 'yyyy-MM-dd');
          const { startUtc: memberTodayStart } = getUtcStartEndOfDate(
            todayStr,
            timeZone,
          );

          const alreadyExists = existingAttendances.some(
            (attendance) =>
              attendance.member_id === member.id &&
              new Date(attendance.date).getTime() === memberTodayStart.getTime(),
          );

          if (alreadyExists) {
            continue;
          }

          const existingCount = await this.db.attendances.count({
            where: {
              member_id: member.id,
              date: memberTodayStart,
            },
          });

          if (existingCount > 0) {
            logger.warn(`Duplicate prevented for member ${member.id}`);
            continue;
          }

          const holidays = ownerHolidayMap.get(ownerId) ?? [];

          let status: AttendanceStatusEnum = AttendanceStatusEnum.absent;

          const isHoliday = holidays.some((holiday) => {
            if (!holiday.departments?.length) return true;
            return holiday.departments.some(
              (d) => d.id === member.department_id,
            );
          });

          if (isHoliday) {
            status = AttendanceStatusEnum.holiday;
          } else {
            const dayOfWeek = Number(formatInTimeZone(now, timeZone, 'i')) % 7; // 0 = Sun ... 6 = Sat
            const todayDaySchedule = workDaySchedules.find(
              (s) => s.owner_id === ownerId && s.day_of_week === dayOfWeek,
            );

            const isWeekend = todayDaySchedule
              ? !todayDaySchedule.is_active
              : false;

            if (isWeekend) {
              status = AttendanceStatusEnum.weekend;
            } else {
              const isOnLeave = approvedLeaves.some(
                (l) =>
                  l.member_id === member.id &&
                  new Date(l.start_date) <= memberTodayStart &&
                  new Date(l.end_date) >= memberTodayStart,
              );
              if (isOnLeave) {
                status = AttendanceStatusEnum.on_leave;
              }
            }
          }

          await this.db.attendances.create({
            data: {
              member_id: member.id,
              owner_id: ownerId,
              date: todayStr,
              status,
              type: AttendanceTypeEnum.in_office,
            },
          });

          logger.log(`Attendance created for member ${member.id} → ${status}`);
        } catch (err: any) {
          if (err?.code === 'ER_DUP_ENTRY' || err?.errno === 1062) {
            logger.warn(`Duplicate prevented for member ${member.id}`);
          } else {
            logger.error(
              `Failed to create attendance for member ${member.id}`,
              err?.stack,
            );
          }
        }
      }

      logger.log('Attendance cron completed successfully');
    } catch (err: any) {
      logger.error('Attendance cron crashed', err?.stack);
    }
  }
}
