import {
  Injectable,
  NotFoundException,
  BadRequestException,
  ConflictException,
} from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { EmployeesService } from '../employees/employees.service';
import { ClockInDto } from './dto/clock-in.dto';
import { ClockOutDto } from './dto/clock-out.dto';
import { CreateTimeEntryDto } from './dto/create-time-entry.dto';
import { UpdateTimeEntryDto } from './dto/update-time-entry.dto';
import { QueryTimeEntriesDto } from './dto/query-time-entries.dto';
import { Prisma, TimeEntryType } from '@prisma/client';

export interface TimeSummary {
  employeeId: string;
  month: number;
  year: number;
  totalWorkingDays: number;
  totalWorkedMinutes: number;
  totalBreakMinutes: number;
  totalOvertimeMinutes: number;
  expectedWorkingMinutes: number;
  entries: {
    date: Date;
    clockIn: Date | null;
    clockOut: Date | null;
    breakMinutes: number;
    workedMinutes: number;
    overtimeMinutes: number;
    type: TimeEntryType;
  }[];
}

export interface CurrentStatus {
  isClockedIn: boolean;
  isOnBreak: boolean;
  todayEntry: {
    id: string;
    date: Date;
    clockIn: Date | null;
    clockOut: Date | null;
    breakMinutes: number;
    type: TimeEntryType;
    note: string | null;
  } | null;
  currentBreakStart: Date | null;
}

@Injectable()
export class TimeTrackingService {
  // German labor law: minimum break requirements
  // > 6 hours: 30 min break
  // > 9 hours: 45 min break
  private readonly BREAK_THRESHOLD_6H = 6 * 60; // 360 minutes
  private readonly BREAK_THRESHOLD_9H = 9 * 60; // 540 minutes
  private readonly MIN_BREAK_6H = 30; // minutes
  private readonly MIN_BREAK_9H = 45; // minutes

  constructor(
    private readonly prisma: PrismaService,
    private readonly employeesService: EmployeesService,
  ) {}

  /**
   * Clock in for the current day
   */
  async clockIn(userId: string, dto: ClockInDto) {
    const employee = await this.getEmployeeByUserId(userId);
    const today = this.getDateOnly(new Date());
    const now = new Date();

    // Check if already clocked in today
    const existingEntry = await this.prisma.timeEntry.findUnique({
      where: {
        employeeId_date: {
          employeeId: employee.id,
          date: today,
        },
      },
    });

    if (existingEntry?.clockIn && !existingEntry.clockOut) {
      throw new ConflictException('Already clocked in for today');
    }

    if (existingEntry?.clockOut) {
      throw new ConflictException(
        'Already have a complete time entry for today. Use manual correction if needed.',
      );
    }

    // Create or update the time entry
    return this.prisma.timeEntry.upsert({
      where: {
        employeeId_date: {
          employeeId: employee.id,
          date: today,
        },
      },
      create: {
        employeeId: employee.id,
        date: today,
        clockIn: now,
        type: TimeEntryType.REGULAR,
        note: dto.note,
      },
      update: {
        clockIn: now,
        note: dto.note,
      },
      include: {
        employee: {
          include: {
            user: {
              select: {
                firstName: true,
                lastName: true,
              },
            },
          },
        },
      },
    });
  }

  /**
   * Clock out for the current day
   */
  async clockOut(userId: string, dto: ClockOutDto) {
    const employee = await this.getEmployeeByUserId(userId);
    const today = this.getDateOnly(new Date());
    const now = new Date();

    const existingEntry = await this.prisma.timeEntry.findUnique({
      where: {
        employeeId_date: {
          employeeId: employee.id,
          date: today,
        },
      },
    });

    if (!existingEntry || !existingEntry.clockIn) {
      throw new BadRequestException('Not clocked in for today');
    }

    if (existingEntry.clockOut) {
      throw new ConflictException('Already clocked out for today');
    }

    if (existingEntry.isLocked) {
      throw new BadRequestException('Time entry is locked and cannot be modified');
    }

    // Calculate worked minutes and apply automatic break rules
    const workedMinutes = this.calculateMinutesBetween(existingEntry.clockIn, now);
    const requiredBreak = this.calculateRequiredBreak(workedMinutes);
    const actualBreak = Math.max(existingEntry.breakMinutes, requiredBreak);

    // Update note if provided
    const note = dto.note
      ? existingEntry.note
        ? `${existingEntry.note} | ${dto.note}`
        : dto.note
      : existingEntry.note;

    return this.prisma.timeEntry.update({
      where: { id: existingEntry.id },
      data: {
        clockOut: now,
        breakMinutes: actualBreak,
        note,
      },
      include: {
        employee: {
          include: {
            user: {
              select: {
                firstName: true,
                lastName: true,
              },
            },
          },
        },
      },
    });
  }

  /**
   * Start a break
   */
  async startBreak(userId: string) {
    const employee = await this.getEmployeeByUserId(userId);
    const today = this.getDateOnly(new Date());

    const entry = await this.prisma.timeEntry.findUnique({
      where: {
        employeeId_date: {
          employeeId: employee.id,
          date: today,
        },
      },
    });

    if (!entry || !entry.clockIn) {
      throw new BadRequestException('Not clocked in for today');
    }

    if (entry.clockOut) {
      throw new BadRequestException('Already clocked out for today');
    }

    // Store break start time in note (simple approach without extra DB field)
    const breakStart = new Date().toISOString();
    const note = entry.note
      ? `${entry.note} | BREAK_START:${breakStart}`
      : `BREAK_START:${breakStart}`;

    await this.prisma.timeEntry.update({
      where: { id: entry.id },
      data: { note },
    });

    return {
      message: 'Break started',
      breakStartedAt: breakStart,
    };
  }

  /**
   * End a break
   */
  async endBreak(userId: string) {
    const employee = await this.getEmployeeByUserId(userId);
    const today = this.getDateOnly(new Date());

    const entry = await this.prisma.timeEntry.findUnique({
      where: {
        employeeId_date: {
          employeeId: employee.id,
          date: today,
        },
      },
    });

    if (!entry || !entry.clockIn) {
      throw new BadRequestException('Not clocked in for today');
    }

    if (entry.clockOut) {
      throw new BadRequestException('Already clocked out for today');
    }

    // Extract break start from note
    const breakStartMatch = entry.note?.match(/BREAK_START:([^\s|]+)/);
    if (!breakStartMatch) {
      throw new BadRequestException('No active break found');
    }

    const breakStart = new Date(breakStartMatch[1]);
    const breakEnd = new Date();
    const breakDuration = this.calculateMinutesBetween(breakStart, breakEnd);

    // Update break minutes and clean note
    const newBreakMinutes = entry.breakMinutes + breakDuration;
    const note = entry.note
      ? entry.note.replace(/\s*\|?\s*BREAK_START:[^\s|]+/, '').trim()
      : null;

    await this.prisma.timeEntry.update({
      where: { id: entry.id },
      data: {
        breakMinutes: newBreakMinutes,
        note: note || null,
      },
    });

    return {
      message: 'Break ended',
      breakDurationMinutes: breakDuration,
      totalBreakMinutes: newBreakMinutes,
    };
  }

  /**
   * Get current time tracking status for a user
   */
  async getCurrentStatus(userId: string): Promise<CurrentStatus> {
    const employee = await this.getEmployeeByUserId(userId);
    const today = this.getDateOnly(new Date());

    const entry = await this.prisma.timeEntry.findUnique({
      where: {
        employeeId_date: {
          employeeId: employee.id,
          date: today,
        },
      },
    });

    if (!entry) {
      return {
        isClockedIn: false,
        isOnBreak: false,
        todayEntry: null,
        currentBreakStart: null,
      };
    }

    // Check for active break
    const breakStartMatch = entry.note?.match(/BREAK_START:([^\s|]+)/);
    const currentBreakStart = breakStartMatch ? new Date(breakStartMatch[1]) : null;

    return {
      isClockedIn: !!entry.clockIn && !entry.clockOut,
      isOnBreak: !!currentBreakStart,
      todayEntry: {
        id: entry.id,
        date: entry.date,
        clockIn: entry.clockIn,
        clockOut: entry.clockOut,
        breakMinutes: entry.breakMinutes,
        type: entry.type,
        note: entry.note,
      },
      currentBreakStart,
    };
  }

  /**
   * Get time entries for the current user
   */
  async getMyEntries(userId: string, query: QueryTimeEntriesDto) {
    const employee = await this.getEmployeeByUserId(userId);
    return this.getEntriesByEmployee(employee.id, query);
  }

  /**
   * Get time entries for a specific employee (HR access)
   */
  async getEntriesByEmployee(employeeId: string, query: QueryTimeEntriesDto) {
    // Verify employee exists
    await this.employeesService.findOne(employeeId);

    const {
      page = 1,
      limit = 20,
      startDate,
      endDate,
      type,
      isLocked,
      sortBy,
      sortOrder,
    } = query;
    const skip = (page - 1) * limit;

    const where: Prisma.TimeEntryWhereInput = {
      employeeId,
      ...(startDate || endDate
        ? {
            date: {
              ...(startDate && { gte: startDate }),
              ...(endDate && { lte: endDate }),
            },
          }
        : {}),
      ...(type && { type }),
      ...(isLocked !== undefined && { isLocked }),
    };

    const orderBy: Prisma.TimeEntryOrderByWithRelationInput = sortBy
      ? { [sortBy]: sortOrder || 'desc' }
      : { date: 'desc' };

    const [entries, total] = await Promise.all([
      this.prisma.timeEntry.findMany({
        where,
        skip,
        take: limit,
        orderBy,
        include: {
          correctedBy: {
            select: {
              id: true,
              firstName: true,
              lastName: true,
            },
          },
        },
      }),
      this.prisma.timeEntry.count({ where }),
    ]);

    return {
      data: entries,
      meta: {
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  /**
   * Create a manual time entry (correction)
   */
  async createManualEntry(dto: CreateTimeEntryDto, correctedById: string) {
    // Verify employee exists
    await this.employeesService.findOne(dto.employeeId);

    const date = this.getDateOnly(dto.date);

    // Check for existing entry
    const existingEntry = await this.prisma.timeEntry.findUnique({
      where: {
        employeeId_date: {
          employeeId: dto.employeeId,
          date,
        },
      },
    });

    if (existingEntry) {
      throw new ConflictException(
        `Time entry already exists for employee on ${date.toISOString().split('T')[0]}`,
      );
    }

    // Validate clock times if both provided
    if (dto.clockIn && dto.clockOut) {
      if (dto.clockOut <= dto.clockIn) {
        throw new BadRequestException('Clock-out must be after clock-in');
      }
    }

    return this.prisma.timeEntry.create({
      data: {
        employeeId: dto.employeeId,
        date,
        clockIn: dto.clockIn,
        clockOut: dto.clockOut,
        breakMinutes: dto.breakMinutes ?? 0,
        type: dto.type ?? TimeEntryType.CORRECTION,
        note: dto.note,
        correctedById,
      },
      include: {
        employee: {
          include: {
            user: {
              select: {
                firstName: true,
                lastName: true,
              },
            },
          },
        },
        correctedBy: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
          },
        },
      },
    });
  }

  /**
   * Update a time entry (correction)
   */
  async updateEntry(
    id: string,
    dto: UpdateTimeEntryDto,
    correctedById: string,
  ) {
    const entry = await this.prisma.timeEntry.findUnique({
      where: { id },
    });

    if (!entry) {
      throw new NotFoundException(`Time entry with ID ${id} not found`);
    }

    if (entry.isLocked && !dto.isLocked) {
      throw new BadRequestException(
        'Cannot modify a locked time entry. Unlock it first.',
      );
    }

    // Validate clock times if both will be present
    const newClockIn = dto.clockIn ?? entry.clockIn;
    const newClockOut = dto.clockOut ?? entry.clockOut;
    if (newClockIn && newClockOut && newClockOut <= newClockIn) {
      throw new BadRequestException('Clock-out must be after clock-in');
    }

    return this.prisma.timeEntry.update({
      where: { id },
      data: {
        ...(dto.clockIn !== undefined && { clockIn: dto.clockIn }),
        ...(dto.clockOut !== undefined && { clockOut: dto.clockOut }),
        ...(dto.breakMinutes !== undefined && { breakMinutes: dto.breakMinutes }),
        ...(dto.type !== undefined && { type: dto.type }),
        ...(dto.note !== undefined && { note: dto.note }),
        ...(dto.isLocked !== undefined && { isLocked: dto.isLocked }),
        correctedById,
      },
      include: {
        employee: {
          include: {
            user: {
              select: {
                firstName: true,
                lastName: true,
              },
            },
          },
        },
        correctedBy: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
          },
        },
      },
    });
  }

  /**
   * Get monthly summary for an employee
   */
  async getMonthlySummary(
    employeeId: string,
    year: number,
    month: number,
  ): Promise<TimeSummary> {
    const employee = await this.employeesService.findOne(employeeId);

    // Get date range for the month
    const startDate = new Date(year, month - 1, 1);
    const endDate = new Date(year, month, 0); // Last day of month

    const entries = await this.prisma.timeEntry.findMany({
      where: {
        employeeId,
        date: {
          gte: startDate,
          lte: endDate,
        },
      },
      orderBy: { date: 'asc' },
    });

    // Calculate working days in month (excluding weekends)
    let totalWorkingDays = 0;
    const current = new Date(startDate);
    while (current <= endDate) {
      const dayOfWeek = current.getDay();
      if (dayOfWeek !== 0 && dayOfWeek !== 6) {
        totalWorkingDays++;
      }
      current.setDate(current.getDate() + 1);
    }

    // Calculate expected working minutes based on employee's working hours
    const weeklyHours = Number(employee.workingHours) || 40;
    const dailyMinutes = (weeklyHours / 5) * 60;
    const expectedWorkingMinutes = Math.round(totalWorkingDays * dailyMinutes);

    // Process entries
    let totalWorkedMinutes = 0;
    let totalBreakMinutes = 0;
    let totalOvertimeMinutes = 0;

    const processedEntries = entries.map((entry) => {
      let workedMinutes = 0;
      let overtimeMinutes = 0;

      if (entry.clockIn && entry.clockOut) {
        const totalMinutes = this.calculateMinutesBetween(
          entry.clockIn,
          entry.clockOut,
        );
        workedMinutes = totalMinutes - entry.breakMinutes;

        // Calculate overtime (anything over daily expected)
        if (workedMinutes > dailyMinutes) {
          overtimeMinutes = workedMinutes - dailyMinutes;
        }
      }

      totalWorkedMinutes += workedMinutes;
      totalBreakMinutes += entry.breakMinutes;
      totalOvertimeMinutes += overtimeMinutes;

      return {
        date: entry.date,
        clockIn: entry.clockIn,
        clockOut: entry.clockOut,
        breakMinutes: entry.breakMinutes,
        workedMinutes,
        overtimeMinutes,
        type: entry.type,
      };
    });

    return {
      employeeId,
      month,
      year,
      totalWorkingDays,
      totalWorkedMinutes,
      totalBreakMinutes,
      totalOvertimeMinutes,
      expectedWorkingMinutes,
      entries: processedEntries,
    };
  }

  /**
   * Helper: Get employee by user ID
   */
  private async getEmployeeByUserId(userId: string) {
    const employee = await this.prisma.employee.findUnique({
      where: { userId },
    });

    if (!employee) {
      throw new NotFoundException(
        'No employee record found for current user',
      );
    }

    if (!employee.isActive) {
      throw new BadRequestException('Employee record is deactivated');
    }

    return employee;
  }

  /**
   * Helper: Get date only (strip time)
   */
  private getDateOnly(date: Date): Date {
    const d = new Date(date);
    d.setHours(0, 0, 0, 0);
    return d;
  }

  /**
   * Helper: Calculate minutes between two dates
   */
  private calculateMinutesBetween(start: Date, end: Date): number {
    return Math.round((end.getTime() - start.getTime()) / (1000 * 60));
  }

  /**
   * Helper: Calculate required break based on German labor law
   */
  private calculateRequiredBreak(workedMinutes: number): number {
    if (workedMinutes > this.BREAK_THRESHOLD_9H) {
      return this.MIN_BREAK_9H;
    }
    if (workedMinutes > this.BREAK_THRESHOLD_6H) {
      return this.MIN_BREAK_6H;
    }
    return 0;
  }
}
