<?php

namespace App\Http\Controllers\Web;

use App\Http\Controllers\Controller;
use App\Models\Attendance;
use App\Models\AttendanceRegularization;
use App\Models\Holiday;
use App\Models\Leave;
use App\Models\User;
use App\Models\WeekoffRule;
use App\Models\WorkDuration;
use App\Services\Attendance\AttendanceCalendarDayResolver;
use App\Services\Attendance\WeekoffResolver;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class AttendanceController extends Controller
{
    public function __construct(
        protected WeekoffResolver $weekoffResolver,
        protected AttendanceCalendarDayResolver $calendarDayResolver
    ) {}

    public function index(Request $request)
    {
        $authUser = Auth::user();
        $companyId = (int) ($authUser?->company_id ?? 0);
        $visibleUserIds = $authUser?->visibleStaffUserIds() ?? [];

        if ($request->filled('date') && ! $request->filled('year') && ! $request->filled('month')) {
            $legacyDate = Carbon::parse($request->date);

            return redirect()->route('attendance.index', [
                'year' => $legacyDate->year,
                'month' => $legacyDate->month,
                'employee_id' => $request->input('employee_id'),
            ]);
        }

        $selectedYear = max(2000, min(2100, (int) $request->integer('year', (int) now()->format('Y'))));
        $selectedMonth = max(1, min(12, (int) $request->integer('month', (int) now()->format('m'))));
        $monthStart = Carbon::createFromDate($selectedYear, $selectedMonth, 1)->startOfDay();
        $monthEnd = $monthStart->copy()->endOfMonth();

        $employeeOptions = User::query()
            ->with('department')
            ->forActiveDirectory()
            ->when($companyId > 0, fn ($query) => $query->where('company_id', $companyId))
            ->when(! empty($visibleUserIds), fn ($query) => $query->whereIn('id', $visibleUserIds))
            ->orderBy('name')
            ->get();

        $canPickEmployee = $employeeOptions->count() > 1;
        $defaultEmployeeId = $this->resolveDefaultEmployeeId($authUser, $employeeOptions);
        $selectedEmployeeId = $canPickEmployee && $request->filled('employee_id')
            ? (int) $request->integer('employee_id')
            : $defaultEmployeeId;

        if (! in_array($selectedEmployeeId, $employeeOptions->pluck('id')->all(), true)) {
            abort(403, 'You cannot view this employee attendance.');
        }

        $selectedEmployee = $employeeOptions->firstWhere('id', $selectedEmployeeId);
        if (! $selectedEmployee) {
            abort(404, 'Employee not found.');
        }

        $holidayMap = $this->holidayDateMap($companyId, $monthStart, $monthEnd);
        $attendanceByDate = $this->attendanceMapWithSessions($companyId, [$selectedEmployeeId], $monthStart, $monthEnd)[$selectedEmployeeId] ?? [];
        $leaveByDate = $this->leaveMap($companyId, [$selectedEmployeeId], $monthStart, $monthEnd)[$selectedEmployeeId] ?? [];

        $regularizationsByDate = \App\Models\AttendanceRegularization::query()
            ->with('reviewer')
            ->where('company_id', $companyId)
            ->where('user_id', $selectedEmployeeId)
            ->whereBetween('date', [$monthStart->toDateString(), $monthEnd->toDateString()])
            ->orderByDesc('created_at')
            ->get()
            ->groupBy(fn ($item) => Carbon::parse($item->date)->toDateString())
            ->map(fn ($items) => $items->first());

        $selectedEmployee->loadMissing('shift');

        $calendarWeeks = $this->buildCalendarWeeks(
            $companyId,
            $selectedEmployeeId,
            $monthStart,
            $monthEnd,
            $holidayMap,
            $attendanceByDate,
            $leaveByDate,
            $selectedEmployee,
            $regularizationsByDate
        );
        $stats = $this->computeMonthStats($calendarWeeks);
        $weekoffRuleName = $this->weekoffResolver->currentRuleName($companyId, $selectedEmployeeId);

        $canRequestRegularization = (int) $authUser?->id === $selectedEmployeeId
            && $authUser?->hasPermission('attendances_view')
            && $authUser?->isWorkforceMember();

        $prevMonth = $monthStart->copy()->subMonth();
        $nextMonth = $monthStart->copy()->addMonth();
        $canGoNextMonth = $nextMonth->lte(Carbon::now()->startOfMonth());

        return view('attendance.index', [
            'stats' => $stats,
            'selectedYear' => $selectedYear,
            'selectedMonth' => $selectedMonth,
            'monthLabel' => $monthStart->format('F Y'),
            'monthValue' => $monthStart->format('Y-m'),
            'selectedEmployee' => $selectedEmployee,
            'selectedEmployeeId' => $selectedEmployeeId,
            'employeeOptions' => $employeeOptions,
            'canPickEmployee' => $canPickEmployee,
            'calendarWeeks' => $calendarWeeks,
            'dayDetails' => $this->flattenCalendarDayDetails($calendarWeeks),
            'regularizationsByDate' => $regularizationsByDate,
            'canRequestRegularization' => $canRequestRegularization,
            'prevMonthUrl' => route('attendance.index', [
                'year' => $prevMonth->year,
                'month' => $prevMonth->month,
                'employee_id' => $selectedEmployeeId,
            ]),
            'nextMonthUrl' => $canGoNextMonth
                ? route('attendance.index', [
                    'year' => $nextMonth->year,
                    'month' => $nextMonth->month,
                    'employee_id' => $selectedEmployeeId,
                ])
                : null,
            'canManageAttendance' => $this->canManageAttendance($authUser),
            'weekoffRuleName' => $weekoffRuleName,
            'markableUsers' => User::query()
                ->with('department')
                ->whereIn('id', $visibleUserIds)
                ->orderBy('name')
                ->get(),
        ]);
    }

    public function checkIn(Request $request)
    {
        $user = Auth::user();
        if (! $user->isWorkforceMember()) {
            if ($request->wantsJson()) {
                return response()->json(['message' => 'Attendance is only available for employee accounts.'], 403);
            }
            return redirect()->back()->with('error', 'Attendance is only available for employee accounts.');
        }
        $companyId = $user->company_id;
        if (! $companyId) {
            if ($request->wantsJson()) {
                return response()->json(['message' => 'No company assigned.'], 403);
            }
            return redirect()->back()->with('error', 'No company assigned.');
        }

        $today = Carbon::today()->toDateString();
        $todayDate = Carbon::today();
        if ($this->isWeekoffDay($companyId, (int) $user->id, $todayDate)) {
            $message = 'Today is a week off as per your weekoff rule.';
            if ($request->wantsJson()) {
                return response()->json(['message' => $message], 422);
            }

            return redirect()->back()->with('error', $message);
        }
        if ($this->isHolidayDay($companyId, $todayDate)) {
            $message = 'Today is a company holiday.';
            if ($request->wantsJson()) {
                return response()->json(['message' => $message], 422);
            }

            return redirect()->back()->with('error', $message);
        }

        $attendance = Attendance::firstOrCreate(
            [
                'company_id' => $companyId,
                'user_id' => $user->id,
                'date' => $today,
            ],
            [
                'status' => 'present',
            ]
        );

        $openSession = WorkDuration::query()
            ->where('company_id', $companyId)
            ->where('attendance_id', $attendance->id)
            ->whereNull('end_time')
            ->first();
        if ($openSession) {
            if ($request->wantsJson()) {
                return response()->json(['message' => 'Active session already running. Please clock out first.', 'attendance' => $attendance], 422);
            }
            return redirect()->back()->with('info', 'You already have an active session. Please clock out first.');
        }

        $now = Carbon::now();
        WorkDuration::create([
            'company_id' => $companyId,
            'attendance_id' => $attendance->id,
            'start_time' => $now->format('H:i:s'),
            'status' => 'open',
        ]);

        // First session of the day initializes summary-level clock-in.
        if (! $attendance->clock_in_date_time) {
            $isLate = $this->calendarDayResolver->isLateArrivalForUser($user, $now);
            $attendance->update([
                'clock_in_date_time' => $now,
                'clock_in_time' => $now->format('H:i:s'),
                'clock_in_ip_address' => $request->ip(),
                'clock_in_latitude' => $request->input('latitude'),
                'clock_in_longitude' => $request->input('longitude'),
                'clock_in_location' => $request->input('location'),
                'status' => 'present',
                'is_late' => $isLate,
            ]);
        }

        if ($request->wantsJson()) {
            return response()->json(['message' => 'Clocked in successfully.', 'attendance' => $attendance->fresh()], 201);
        }
        return redirect()->back()->with('success', 'Clocked in at ' . $now->format('g:i A'));
    }

    public function checkOut(Request $request)
    {
        $user = Auth::user();
        if (! $user->isWorkforceMember()) {
            if ($request->wantsJson()) {
                return response()->json(['message' => 'Attendance is only available for employee accounts.'], 403);
            }
            return redirect()->back()->with('error', 'Attendance is only available for employee accounts.');
        }
        $companyId = $user->company_id;
        if (! $companyId) {
            if ($request->wantsJson()) {
                return response()->json(['message' => 'No company assigned.'], 403);
            }
            return redirect()->back()->with('error', 'No company assigned.');
        }

        $today = Carbon::today()->toDateString();
        $attendance = Attendance::query()
            ->where('company_id', $companyId)
            ->where('user_id', $user->id)
            ->where('date', $today)
            ->whereNotNull('clock_in_date_time')
            ->first();

        if (! $attendance) {
            if ($request->wantsJson()) {
                return response()->json(['message' => 'No clock-in found for today.'], 422);
            }
            return redirect()->back()->with('error', 'Clock in first before clocking out.');
        }

        $openSession = WorkDuration::query()
            ->where('company_id', $companyId)
            ->where('attendance_id', $attendance->id)
            ->whereNull('end_time')
            ->latest('id')
            ->first();
        if (! $openSession) {
            if ($request->wantsJson()) {
                return response()->json(['message' => 'No active session found.'], 422);
            }
            return redirect()->back()->with('error', 'No active session found. Clock in first.');
        }

        $now = Carbon::now();
        $sessionStart = Carbon::parse($today . ' ' . $openSession->start_time);
        $sessionDuration = max(0, $sessionStart->diffInSeconds($now));

        $openSession->update([
            'end_time' => $now->format('H:i:s'),
            'duration' => $sessionDuration,
            'status' => 'closed',
        ]);

        $totalDuration = (int) WorkDuration::query()
            ->where('company_id', $companyId)
            ->where('attendance_id', $attendance->id)
            ->sum('duration');

        $attendance->update([
            'clock_out_date_time' => $now,
            'clock_out_time' => $now->format('H:i:s'),
            'clock_out_ip_address' => $request->ip(),
            'clock_out_latitude' => $request->input('latitude'),
            'clock_out_longitude' => $request->input('longitude'),
            'clock_out_location' => $request->input('location'),
            'total_duration' => $totalDuration,
        ]);

        if ($request->wantsJson()) {
            return response()->json(['message' => 'Clocked out successfully.', 'attendance' => $attendance->fresh()], 200);
        }
        return redirect()->back()->with('success', 'Clocked out at ' . $now->format('g:i A'));
    }

    public function createBulk(Request $request)
    {
        $authUser = Auth::user();
        if (! $this->canManageAttendance($authUser)) {
            abort(403);
        }

        $selectedYear = max(2000, min(2100, (int) $request->integer('year', (int) now()->format('Y'))));
        $selectedMonth = max(1, min(12, (int) $request->integer('month', (int) now()->format('m'))));
        $selectedUserId = $request->filled('user_id') ? (int) $request->integer('user_id') : null;
        $selectedDepartmentId = $request->filled('department_id') ? (int) $request->integer('department_id') : null;
        $selectedLocationId = $request->filled('location_id') ? (int) $request->integer('location_id') : null;

        $monthStart = Carbon::createFromDate($selectedYear, $selectedMonth, 1)->startOfDay();
        $monthEnd = $monthStart->copy()->endOfMonth();
        $companyId = (int) ($authUser?->company_id ?? 0);
        $baseUsersQuery = User::query()
            ->with(['department', 'location'])
            ->whereIn('id', $authUser->visibleStaffUserIds());

        $filterOptionUsers = (clone $baseUsersQuery)->get();
        $departmentOptions = $filterOptionUsers
            ->pluck('department')
            ->filter()
            ->unique('id')
            ->sortBy('name')
            ->values();
        $locationOptions = $filterOptionUsers
            ->pluck('location')
            ->filter()
            ->unique('id')
            ->sortBy('name')
            ->values();

        if ($selectedDepartmentId) {
            $baseUsersQuery->where('department_id', $selectedDepartmentId);
        }
        if ($selectedLocationId) {
            $baseUsersQuery->where('location_id', $selectedLocationId);
        }
        if ($selectedUserId) {
            $baseUsersQuery->where('id', $selectedUserId);
        }

        $users = $baseUsersQuery
            ->orderBy('name')
            ->get()
            ->values();

        $markableUsers = $selectedDepartmentId || $selectedLocationId
            ? $filterOptionUsers
                ->when($selectedDepartmentId, fn ($items) => $items->where('department_id', $selectedDepartmentId))
                ->when($selectedLocationId, fn ($items) => $items->where('location_id', $selectedLocationId))
                ->sortBy('name')
                ->values()
            : $filterOptionUsers
                ->sortBy('name')
                ->values();

        $days = $this->buildMonthDays($monthStart, $monthEnd);
        $holidayMap = $this->holidayDateMap($companyId, $monthStart, $monthEnd);
        $attendanceMap = $this->attendanceMap($companyId, $users->pluck('id')->all(), $monthStart, $monthEnd);
        $leaveMap = $this->leaveMap($companyId, $users->pluck('id')->all(), $monthStart, $monthEnd);
        $weekoffMaps = $this->weekoffResolver->dayTypeMapsForUsers(
            $companyId,
            $users->pluck('id')->all(),
            $monthStart,
            $monthEnd
        );

        $matrixRows = $users->map(function (User $user) use ($days, $holidayMap, $attendanceMap, $leaveMap, $weekoffMaps, $companyId) {
            $workingDays = 0;
            $presentUnits = 0.0;
            $cells = [];
            $today = Carbon::today()->toDateString();
            $userWeekoffMap = $weekoffMaps[$user->id] ?? [];

            foreach ($days as $day) {
                $date = $day['date'];
                $scheduledDayType = $userWeekoffMap[$date] ?? WeekoffRule::DAY_WORKING;
                $isWeekoff = $scheduledDayType === WeekoffRule::DAY_WEEKLY_OFF;
                $status = $this->resolveBulkStatus(
                    $date,
                    $isWeekoff,
                    $holidayMap,
                    $attendanceMap[$user->id] ?? [],
                    $leaveMap[$user->id] ?? [],
                    $scheduledDayType
                );
                if (! in_array($status, ['weekoff', 'holiday'], true)) {
                    $workingDays++;
                }
                if ($status === 'present') {
                    $presentUnits += 1;
                } elseif ($status === 'half_day') {
                    $presentUnits += 0.5;
                }
                $cells[] = [
                    'date' => $date,
                    'status' => $status,
                    'can_toggle' => ! in_array($status, ['weekoff', 'holiday'], true) && $date <= $today,
                ];
            }

            return [
                'user' => $user,
                'cells' => $cells,
                'present_units' => $presentUnits,
                'working_days' => $workingDays,
                'total_label' => rtrim(rtrim(number_format($presentUnits, 1, '.', ''), '0'), '.') . ' / ' . $workingDays,
            ];
        })->values();

        return view('attendance.create-bulk', [
            'markableUsers' => $markableUsers,
            'employeeOptions' => $filterOptionUsers
                ->sortBy('name')
                ->values()
                ->map(fn (User $user) => [
                    'id' => (int) $user->id,
                    'name' => (string) $user->name,
                    'department_id' => $user->department_id ? (int) $user->department_id : null,
                    'location_id' => $user->location_id ? (int) $user->location_id : null,
                ])
                ->all(),
            'matrixRows' => $matrixRows,
            'days' => $days,
            'selectedYear' => $selectedYear,
            'selectedMonth' => $selectedMonth,
            'selectedUserId' => $selectedUserId,
            'selectedDepartmentId' => $selectedDepartmentId,
            'selectedLocationId' => $selectedLocationId,
            'departmentOptions' => $departmentOptions,
            'locationOptions' => $locationOptions,
            'canManageAttendance' => true,
        ]);
    }

    public function toggleBulkStatus(Request $request)
    {
        $authUser = Auth::user();
        if (! $this->canManageAttendance($authUser)) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $validated = $request->validate([
            'user_id' => ['required', 'integer'],
            'date' => ['required', 'date_format:Y-m-d'],
            'next_status' => ['required', 'in:present,absent,leave,half_day'],
        ]);

        $companyId = (int) ($authUser?->company_id ?? 0);
        $allowedUserIds = $authUser->visibleStaffUserIds();
        $userId = (int) $validated['user_id'];
        if (! in_array($userId, $allowedUserIds, true)) {
            return response()->json(['message' => 'You cannot update this employee attendance.'], 403);
        }

        $date = Carbon::parse($validated['date'])->toDateString();
        $dateObj = Carbon::parse($date);
        if ($dateObj->isAfter(Carbon::today())) {
            return response()->json(['message' => 'Future attendance dates are not allowed.'], 422);
        }
        if ($this->isWeekoffDay($companyId, $userId, $dateObj)) {
            return response()->json(['message' => 'Week off cannot be modified.'], 422);
        }
        if ($this->isHolidayDay($companyId, $dateObj)) {
            return response()->json(['message' => 'Holiday cannot be modified.'], 422);
        }

        $existing = Attendance::query()
            ->where('company_id', $companyId)
            ->where('user_id', $userId)
            ->where('date', $date)
            ->first();

        $nextStatus = (string) $validated['next_status'];
        if ($nextStatus === 'absent') {
            if ($existing) {
                WorkDuration::query()
                    ->where('company_id', $companyId)
                    ->where('attendance_id', $existing->id)
                    ->delete();
                $existing->delete();
            }
        } elseif ($nextStatus === 'leave') {
            $attendance = Attendance::updateOrCreate(
                ['company_id' => $companyId, 'user_id' => $userId, 'date' => $date],
                [
                    'status' => 'leave',
                    'is_leave' => true,
                    'is_half_day' => false,
                    'clock_in_date_time' => null,
                    'clock_out_date_time' => null,
                    'clock_in_time' => null,
                    'clock_out_time' => null,
                    'clock_in_ip_address' => null,
                    'clock_out_ip_address' => null,
                    'total_duration' => null,
                ]
            );
            WorkDuration::query()
                ->where('company_id', $companyId)
                ->where('attendance_id', $attendance->id)
                ->delete();
        } else {
            $this->upsertAttendanceForDate(
                $companyId,
                $userId,
                $date,
                [
                    'clock_in_time' => '09:00',
                    'clock_out_time' => '18:00',
                    'clock_in_ip_address' => null,
                    'clock_out_ip_address' => null,
                    'is_late' => false,
                    'is_half_day' => $nextStatus === 'half_day',
                ],
                true
            );
            Attendance::query()
                ->where('company_id', $companyId)
                ->where('user_id', $userId)
                ->where('date', $date)
                ->update(['status' => 'present', 'is_leave' => false]);
        }

        $monthStart = Carbon::parse($date)->startOfMonth()->startOfDay();
        $monthEnd = Carbon::parse($date)->endOfMonth()->endOfDay();
        $days = $this->buildMonthDays($monthStart, $monthEnd);
        $holidayMap = $this->holidayDateMap($companyId, $monthStart, $monthEnd);
        $attendanceMap = $this->attendanceMap($companyId, [$userId], $monthStart, $monthEnd);
        $leaveMap = $this->leaveMap($companyId, [$userId], $monthStart, $monthEnd);
        $weekoffMap = $this->weekoffResolver->dayTypeMap($companyId, $userId, $monthStart, $monthEnd);

        $presentUnits = 0.0;
        $workingDays = 0;
        $statusForDate = 'absent';
        foreach ($days as $day) {
            $scheduledDayType = $weekoffMap[$day['date']] ?? WeekoffRule::DAY_WORKING;
            $isWeekoff = $scheduledDayType === WeekoffRule::DAY_WEEKLY_OFF;
            $status = $this->resolveBulkStatus(
                $day['date'],
                $isWeekoff,
                $holidayMap,
                $attendanceMap[$userId] ?? [],
                $leaveMap[$userId] ?? [],
                $scheduledDayType
            );
            if ($day['date'] === $date) {
                $statusForDate = $status;
            }
            if (! in_array($status, ['weekoff', 'holiday'], true)) {
                $workingDays++;
            }
            if ($status === 'present') {
                $presentUnits += 1;
            } elseif ($status === 'half_day') {
                $presentUnits += 0.5;
            }
        }

        return response()->json([
            'message' => 'Attendance updated successfully.',
            'status' => $statusForDate,
            'total_label' => rtrim(rtrim(number_format($presentUnits, 1, '.', ''), '0'), '.') . ' / ' . $workingDays,
        ]);
    }

    public function markMultiDay(Request $request)
    {
        $authUser = Auth::user();
        if (! $this->canManageAttendance($authUser)) {
            abort(403);
        }
        $companyId = $authUser->company_id;
        $allowedUserIds = $authUser->visibleStaffUserIds();

        $validated = $request->validate([
            'user_ids' => ['required', 'array', 'min:1'],
            'user_ids.*' => ['integer'],
            'date' => ['required', 'date'],
            'clock_in_time' => ['required', 'date_format:H:i'],
            'clock_out_time' => ['nullable', 'date_format:H:i'],
            'clock_in_ip_address' => ['nullable', 'string', 'max:45'],
            'clock_out_ip_address' => ['nullable', 'string', 'max:45'],
            'is_late' => ['nullable', 'boolean'],
            'is_half_day' => ['nullable', 'boolean'],
            'overwrite' => ['nullable', 'boolean'],
        ]);

        $selectedIds = collect($validated['user_ids'])
            ->map(fn ($id) => (int) $id)
            ->intersect($allowedUserIds)
            ->values()
            ->all();

        if (empty($selectedIds)) {
            return redirect()->route('attendance.index', $this->attendanceIndexParams($validated['date'], null))
                ->with('error', 'No valid users selected for attendance.');
        }

        $date = Carbon::parse($validated['date'])->toDateString();
        $dateObj = Carbon::parse($date);
        if ($dateObj->isAfter(Carbon::today())) {
            return redirect()->route('attendance.index', $this->attendanceIndexParams(Carbon::today()->toDateString(), null))
                ->withErrors(['date' => 'Future attendance dates are not allowed.'])
                ->withInput();
        }
        $affected = 0;
        $skippedWeekoff = 0;

        foreach ($selectedIds as $userId) {
            if ($this->isWeekoffDay($companyId, $userId, $dateObj) || $this->isHolidayDay($companyId, $dateObj)) {
                $skippedWeekoff++;
                continue;
            }
            $affected += $this->upsertAttendanceForDate(
                $companyId,
                $userId,
                $date,
                $validated,
                (bool) ($validated['overwrite'] ?? false)
            );
        }

        $message = "Attendance marked for {$affected} users.";
        if ($skippedWeekoff > 0) {
            $message .= " Skipped {$skippedWeekoff} user(s) on week off/holiday.";
        }

        return redirect()->route('attendance.index', $this->attendanceIndexParams($date, $selectedIds[0] ?? null))
            ->with('success', $message);
    }

    public function markBulk(Request $request)
    {
        $authUser = Auth::user();
        if (! $this->canManageAttendance($authUser)) {
            abort(403);
        }
        $companyId = $authUser->company_id;
        $allowedUserIds = $authUser->visibleStaffUserIds();

        $validated = $request->validate([
            'mark_by' => ['required', 'in:date,month'],
            'user_ids' => ['nullable', 'array'],
            'user_ids.*' => ['integer'],
            'start_date' => ['nullable', 'date'],
            'end_date' => ['nullable', 'date'],
            'month' => ['nullable', 'date_format:Y-m'],
            'selected_dates' => ['nullable', 'array'],
            'selected_dates.*' => ['date_format:Y-m-d'],
            'clock_in_time' => ['required', 'date_format:H:i'],
            'clock_out_time' => ['nullable', 'date_format:H:i'],
            'clock_in_ip_address' => ['nullable', 'string', 'max:45'],
            'clock_out_ip_address' => ['nullable', 'string', 'max:45'],
            'is_late' => ['nullable', 'boolean'],
            'is_half_day' => ['nullable', 'boolean'],
            'overwrite' => ['nullable', 'boolean'],
        ]);
        if ($validated['mark_by'] === 'month') {
            if (empty($validated['month'])) {
                return back()
                    ->withErrors(['month' => 'Month is required for month mode.'])
                    ->withInput();
            }
            if (empty($validated['selected_dates'])) {
                return back()
                    ->withErrors(['selected_dates' => 'Select at least one date for month mode.'])
                    ->withInput();
            }
        }

        $selectedIds = collect($validated['user_ids'] ?? [])
            ->map(fn ($id) => (int) $id)
            ->intersect($allowedUserIds)
            ->values()
            ->all();
        if (empty($selectedIds)) {
            $selectedIds = $allowedUserIds;
        }

        if (empty($selectedIds)) {
            return redirect()->route('attendance.index')->with('error', 'No users available for bulk attendance.');
        }

        $dates = [];
        if ($validated['mark_by'] === 'month') {
            $monthStr = (string) $validated['month'];
            $selectedDates = array_values(array_unique(array_map('strval', $validated['selected_dates'] ?? [])));
            $selectedDates = array_values(array_filter($selectedDates, fn ($d) => str_starts_with($d, $monthStr . '-')));
            sort($selectedDates);

            if (empty($selectedDates)) {
                return back()
                    ->withErrors(['selected_dates' => 'Selected dates must belong to the chosen month.'])
                    ->withInput();
            }
            $dates = $selectedDates;
        } else {
            $start = Carbon::parse((string) ($validated['start_date'] ?? now()->toDateString()))->startOfDay();
            $end = Carbon::parse((string) ($validated['end_date'] ?? $start->toDateString()))->startOfDay();
            if ($start->gt($end)) {
                [$start, $end] = [$end, $start];
            }
            $cursor = $start->copy();
            while ($cursor->lte($end)) {
                $dates[] = $cursor->toDateString();
                $cursor->addDay();
            }
        }

        $today = Carbon::today()->toDateString();
        $futureDates = array_values(array_filter($dates, fn (string $date) => $date > $today));
        if (! empty($futureDates)) {
            $errorKey = $validated['mark_by'] === 'month' ? 'selected_dates' : 'end_date';
            return back()
                ->withErrors([$errorKey => 'Future attendance dates are not allowed.'])
                ->withInput();
        }

        $affected = 0;
        $skipped = 0;
        foreach ($dates as $date) {
            $dateObj = Carbon::parse($date);
            foreach ($selectedIds as $userId) {
                if ($this->isWeekoffDay($companyId, $userId, $dateObj) || $this->isHolidayDay($companyId, $dateObj)) {
                    $skipped++;
                    continue;
                }
                $affected += $this->upsertAttendanceForDate(
                    $companyId,
                    $userId,
                    $date,
                    $validated,
                    (bool) ($validated['overwrite'] ?? false)
                );
            }
        }

        $message = "Bulk attendance processed for {$affected} records.";
        if ($skipped > 0) {
            $message .= " Skipped {$skipped} week off/holiday day(s).";
        }

        return redirect()->route('attendance.index', $this->attendanceIndexParams($dates[0] ?? now()->toDateString(), $selectedIds[0] ?? null))
            ->with('success', $message);
    }

    protected function canManageAttendance(?User $authUser): bool
    {
        if (! $authUser || ! $authUser->company_id) {
            return false;
        }
        $roleName = $authUser->role?->name ?? '';
        return $authUser->isTenantAdmin() || $roleName === 'manager';
    }

    protected function upsertAttendanceForDate(
        int $companyId,
        int $userId,
        string $date,
        array $payload,
        bool $overwrite
    ): int {
        $dateObj = Carbon::parse($date);
        if ($this->isWeekoffDay($companyId, $userId, $dateObj) || $this->isHolidayDay($companyId, $dateObj)) {
            return 0;
        }

        $existing = Attendance::query()
            ->where('company_id', $companyId)
            ->where('user_id', $userId)
            ->where('date', $date)
            ->first();

        if ($existing && ! $overwrite && $existing->clock_in_date_time) {
            return 0;
        }

        $clockIn = Carbon::parse($date . ' ' . $payload['clock_in_time']);
        $clockOut = ! empty($payload['clock_out_time'])
            ? Carbon::parse($date . ' ' . $payload['clock_out_time'])
            : null;

        $updateData = [
            'clock_in_date_time' => $clockIn,
            'clock_in_time' => $clockIn->format('H:i:s'),
            'clock_in_ip_address' => $payload['clock_in_ip_address'] ?? ($existing?->clock_in_ip_address),
            'is_late' => (bool) ($payload['is_late'] ?? false),
            'is_half_day' => (bool) ($payload['is_half_day'] ?? false),
            'status' => 'present',
        ];

        if ($clockOut) {
            $updateData['clock_out_date_time'] = $clockOut;
            $updateData['clock_out_time'] = $clockOut->format('H:i:s');
            $updateData['clock_out_ip_address'] = $payload['clock_out_ip_address'] ?? ($existing?->clock_out_ip_address);
            $updateData['total_duration'] = $clockIn->diffInSeconds($clockOut);
        } elseif ($overwrite) {
            $updateData['clock_out_date_time'] = null;
            $updateData['clock_out_time'] = null;
            $updateData['clock_out_ip_address'] = null;
            $updateData['total_duration'] = null;
        }

        $attendance = Attendance::updateOrCreate(
            ['company_id' => $companyId, 'user_id' => $userId, 'date' => $date],
            $updateData
        );

        if ($overwrite) {
            WorkDuration::query()
                ->where('company_id', $companyId)
                ->where('attendance_id', $attendance->id)
                ->delete();
        }

        // Keep popup history in sync by writing at least one work session for marked attendance.
        $sessionData = [
            'company_id' => $companyId,
            'attendance_id' => $attendance->id,
            'start_time' => $clockIn->format('H:i:s'),
            'end_time' => $clockOut?->format('H:i:s'),
            'duration' => $clockOut ? $clockIn->diffInSeconds($clockOut) : null,
            'status' => $clockOut ? 'closed' : 'open',
        ];

        if ($overwrite) {
            WorkDuration::create($sessionData);
        } else {
            $sessionExists = WorkDuration::query()
                ->where('company_id', $companyId)
                ->where('attendance_id', $attendance->id)
                ->where('start_time', $sessionData['start_time'])
                ->where('end_time', $sessionData['end_time'])
                ->exists();
            if (! $sessionExists) {
                WorkDuration::create($sessionData);
            }
        }

        $attendance->update([
            'total_duration' => (int) WorkDuration::query()
                ->where('company_id', $companyId)
                ->where('attendance_id', $attendance->id)
                ->sum('duration'),
        ]);

        return 1;
    }

    protected function buildMonthDays(Carbon $start, Carbon $end): array
    {
        $days = [];
        $cursor = $start->copy();
        while ($cursor->lte($end)) {
            $days[] = [
                'date' => $cursor->toDateString(),
                'day' => $cursor->format('d'),
            ];
            $cursor->addDay();
        }

        return $days;
    }

    protected function holidayDateMap(int $companyId, Carbon $start, Carbon $end): array
    {
        if ($companyId <= 0) {
            return [];
        }

        return Holiday::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereBetween('date', [$start->toDateString(), $end->toDateString()])
            ->pluck('date')
            ->map(fn ($date) => $date instanceof \DateTimeInterface ? $date->format('Y-m-d') : (string) $date)
            ->flip()
            ->all();
    }

    protected function attendanceMap(int $companyId, array $userIds, Carbon $start, Carbon $end): array
    {
        if ($companyId <= 0 || empty($userIds)) {
            return [];
        }

        $grouped = Attendance::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereIn('user_id', $userIds)
            ->whereBetween('date', [$start->toDateString(), $end->toDateString()])
            ->get()
            ->groupBy('user_id');

        $result = [];
        foreach ($grouped as $userId => $items) {
            $result[(int) $userId] = $items
                ->keyBy(fn (Attendance $attendance) => Carbon::parse($attendance->date)->toDateString())
                ->all();
        }

        return $result;
    }

    protected function leaveMap(int $companyId, array $userIds, Carbon $start, Carbon $end): array
    {
        if ($companyId <= 0 || empty($userIds)) {
            return [];
        }

        $result = [];
        $leaves = Leave::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereIn('user_id', $userIds)
            ->where('status', 'approved')
            ->where(function ($query) use ($start, $end) {
                $query->whereBetween('start_date', [$start->toDateString(), $end->toDateString()])
                    ->orWhereBetween('end_date', [$start->toDateString(), $end->toDateString()])
                    ->orWhere(function ($nested) use ($start, $end) {
                        $nested->where('start_date', '<=', $start->toDateString())
                            ->where('end_date', '>=', $end->toDateString());
                    });
            })
            ->get();

        foreach ($leaves as $leave) {
            $leaveStart = Carbon::parse($leave->start_date)->startOfDay();
            $leaveEnd = $leave->end_date
                ? Carbon::parse($leave->end_date)->endOfDay()
                : $leaveStart->copy()->endOfDay();

            $cursor = $leaveStart->gt($start) ? $leaveStart->copy() : $start->copy();
            $last = $leaveEnd->lt($end) ? $leaveEnd->copy() : $end->copy();
            while ($cursor->lte($last)) {
                $result[(int) $leave->user_id][$cursor->toDateString()] = $leave->is_half_day ? 'half_day' : 'leave';
                $cursor->addDay();
            }
        }

        return $result;
    }

    protected function resolveBulkStatus(
        string $date,
        bool $isWeekoff,
        array $holidayMap,
        array $attendanceByDate,
        array $leaveByDate,
        ?string $scheduledDayType = null
    ): string {
        if (isset($holidayMap[$date])) {
            return 'holiday';
        }
        if ($isWeekoff) {
            return 'weekoff';
        }

        /** @var Attendance|null $attendance */
        $attendance = $attendanceByDate[$date] ?? null;
        if ($attendance) {
            if ($attendance->is_leave || $attendance->status === 'leave') {
                return 'leave';
            }
            if ($attendance->clock_in_date_time) {
                return $attendance->is_half_day ? 'half_day' : 'present';
            }
        }

        if (isset($leaveByDate[$date])) {
            return $leaveByDate[$date] === 'half_day' ? 'half_day' : 'leave';
        }

        if ($scheduledDayType === WeekoffRule::DAY_HALF) {
            return 'half_day';
        }

        return 'absent';
    }

    protected function isWeekoffDay(int $companyId, int $userId, Carbon $date): bool
    {
        return $this->weekoffResolver->isWeekoff($companyId, $userId, $date);
    }

    protected function isHolidayDay(int $companyId, Carbon $date): bool
    {
        if ($companyId <= 0) {
            return false;
        }

        return Holiday::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereDate('date', $date->toDateString())
            ->exists();
    }

    protected function resolveDefaultEmployeeId(?User $authUser, $employeeOptions): int
    {
        if ($employeeOptions->isEmpty()) {
            return (int) ($authUser?->id ?? 0);
        }

        $visibleIds = $employeeOptions->pluck('id')->all();
        if ($authUser && in_array($authUser->id, $visibleIds, true) && $authUser->isWorkforceMember()) {
            return (int) $authUser->id;
        }

        return (int) $employeeOptions->first()->id;
    }

    /**
     * @return array<int, array<string, Attendance>>
     */
    protected function attendanceMapWithSessions(int $companyId, array $userIds, Carbon $start, Carbon $end): array
    {
        if ($companyId <= 0 || empty($userIds)) {
            return [];
        }

        $grouped = Attendance::withoutGlobalScopes()
            ->with('workDurations')
            ->where('company_id', $companyId)
            ->whereIn('user_id', $userIds)
            ->whereBetween('date', [$start->toDateString(), $end->toDateString()])
            ->get()
            ->groupBy('user_id');

        $result = [];
        foreach ($grouped as $userId => $items) {
            $result[(int) $userId] = $items
                ->keyBy(fn (Attendance $attendance) => Carbon::parse($attendance->date)->toDateString())
                ->all();
        }

        return $result;
    }

    protected function buildCalendarWeeks(
        int $companyId,
        int $userId,
        Carbon $monthStart,
        Carbon $monthEnd,
        array $holidayMap,
        array $attendanceByDate,
        array $leaveByDate,
        User $employee,
        $regularizationsByDate = []
    ): array {
        $today = Carbon::today()->toDateString();
        $cellsByDate = [];
        $weekoffMap = $this->weekoffResolver->dayTypeMap($companyId, $userId, $monthStart, $monthEnd);

        foreach ($this->buildMonthDays($monthStart, $monthEnd) as $day) {
            $date = $day['date'];
            $isFuture = $date > $today;
            $scheduledDayType = $weekoffMap[$date] ?? WeekoffRule::DAY_WORKING;
            $isWeekoff = $scheduledDayType === WeekoffRule::DAY_WEEKLY_OFF;
            $bulkStatus = $this->resolveBulkStatus(
                $date,
                $isWeekoff,
                $holidayMap,
                $attendanceByDate,
                $leaveByDate,
                $scheduledDayType
            );

            /** @var Attendance|null $attendance */
            $attendance = $attendanceByDate[$date] ?? null;
            $detail = $this->buildDayAttendanceDetail($attendance, $date);
            $status = $isFuture ? 'future' : $bulkStatus;
            $regularization = $regularizationsByDate instanceof \Illuminate\Support\Collection
                ? $regularizationsByDate->get($date)
                : ($regularizationsByDate[$date] ?? null);

            $resolved = $isFuture
                ? [
                    'visual_status' => '',
                    'display_status' => '',
                    'regularization_status' => null,
                    'icon' => '',
                    'flags' => [],
                    'tooltip' => '',
                ]
                : $this->calendarDayResolver->resolve(
                    $bulkStatus,
                    $date,
                    $attendance,
                    $regularization,
                    $employee,
                    $detail
                );

            $cellsByDate[$date] = [
                'date' => $date,
                'day' => (int) Carbon::parse($date)->format('j'),
                'in_month' => true,
                'is_today' => $date === $today,
                'is_future' => $isFuture,
                'status' => $status,
                'bulk_status' => $bulkStatus,
                'visual_status' => $resolved['visual_status'],
                'display_status' => $resolved['display_status'],
                'regularization_status' => $resolved['regularization_status'],
                'icon' => $resolved['icon'],
                'flags' => $resolved['flags'],
                'clock_in' => $detail['clock_in'],
                'clock_out' => $detail['clock_out'],
                'clock_in_label' => $detail['clock_in_label'],
                'clock_out_label' => $detail['clock_out_label'],
                'hours' => $detail['hours'],
                'sessions_detailed' => $detail['sessions_detailed'],
                'is_clickable' => ! $isFuture && $resolved['display_status'] !== '',
                'tooltip' => $resolved['tooltip'],
            ];
        }

        $gridStart = $monthStart->copy()->startOfWeek(Carbon::SUNDAY);
        $gridEnd = $monthEnd->copy()->endOfWeek(Carbon::SUNDAY);
        $weeks = [];
        $week = [];
        $cursor = $gridStart->copy();

        while ($cursor->lte($gridEnd)) {
            $dateStr = $cursor->toDateString();
            if (isset($cellsByDate[$dateStr])) {
                $week[] = $cellsByDate[$dateStr];
            } else {
                $week[] = [
                    'date' => $dateStr,
                    'day' => (int) $cursor->format('j'),
                    'in_month' => false,
                    'is_today' => $dateStr === $today,
                    'is_future' => $dateStr > $today,
                    'status' => 'outside',
                    'bulk_status' => 'outside',
                    'visual_status' => '',
                    'display_status' => '',
                    'regularization_status' => null,
                    'icon' => '',
                    'flags' => [],
                    'clock_in' => null,
                    'clock_out' => null,
                    'clock_in_label' => null,
                    'clock_out_label' => null,
                    'hours' => null,
                    'sessions_detailed' => [],
                    'is_clickable' => false,
                    'tooltip' => '',
                ];
            }

            if (count($week) === 7) {
                $weeks[] = $week;
                $week = [];
            }

            $cursor->addDay();
        }

        return $weeks;
    }

    /**
     * @return array{
     *     clock_in: ?\Carbon\Carbon,
     *     clock_out: ?\Carbon\Carbon,
     *     clock_in_label: ?string,
     *     clock_out_label: ?string,
     *     hours: string,
     *     sessions_detailed: array<int, array<string, mixed>>,
     *     has_data: bool
     * }
     */
    protected function buildDayAttendanceDetail(?Attendance $attendance, string $date): array
    {
        $clockIn = $attendance?->clock_in_date_time;
        $clockOut = $attendance?->clock_out_date_time;
        $hours = '--';
        $sessionsDetailed = [];

        if ($clockIn) {
            if ($clockOut) {
                $seconds = (int) ($attendance?->total_duration ?? $clockIn->diffInSeconds($clockOut));
                $h = intdiv($seconds, 3600);
                $m = intdiv($seconds % 3600, 60);
                $hours = "{$h}h {$m}m";
            } else {
                $hours = 'In Progress';
            }
        }

        if ($attendance) {
            $sorted = $attendance->workDurations->sortBy('id')->values();
            $sessionsDetailed = $sorted->map(function (WorkDuration $session) use ($date) {
                $start = $session->start_time
                    ? Carbon::parse($date . ' ' . $session->start_time)->format('h:i A')
                    : null;
                $end = $session->end_time
                    ? Carbon::parse($date . ' ' . $session->end_time)->format('h:i A')
                    : null;
                $seconds = (int) ($session->duration ?? 0);
                $h = intdiv($seconds, 3600);
                $m = intdiv($seconds % 3600, 60);
                $durationLabel = $seconds > 0 ? ($h > 0 ? "{$h} hrs {$m} mins" : "{$m} mins") : '--';

                return [
                    'start' => $start,
                    'end' => $end,
                    'duration_seconds' => $seconds,
                    'duration_label' => $durationLabel,
                    'notes' => $session->notes ?? null,
                    'status' => $session->status ?? null,
                ];
            })->values()->all();
        }

        $hasData = (bool) $clockIn
            || ! empty($sessionsDetailed)
            || ($attendance && ($attendance->is_leave || $attendance->status === 'leave'));

        return [
            'clock_in' => $clockIn,
            'clock_out' => $clockOut,
            'clock_in_label' => $clockIn?->format('h:i A'),
            'clock_out_label' => $clockOut?->format('h:i A'),
            'hours' => $hours,
            'sessions_detailed' => $sessionsDetailed,
            'has_data' => $hasData,
        ];
    }

    /**
     * @param  array<int, array<int, array<string, mixed>>>  $calendarWeeks
     * @return array<string, array<string, mixed>>
     */
    protected function flattenCalendarDayDetails(array $calendarWeeks): array
    {
        $details = [];
        foreach ($calendarWeeks as $week) {
            foreach ($week as $cell) {
                if (($cell['in_month'] ?? false) && ($cell['is_clickable'] ?? false)) {
                    $details[$cell['date']] = $cell;
                }
            }
        }

        return $details;
    }

    /**
     * @param  array<int, array<int, array<string, mixed>>>  $calendarWeeks
     * @return array{present: int, absent: int, on_leave: int, late: int, missing_punch_out: int, regularization_pending: int}
     */
    protected function computeMonthStats(array $calendarWeeks): array
    {
        $stats = [
            'present' => 0,
            'absent' => 0,
            'on_leave' => 0,
            'late' => 0,
            'missing_punch_out' => 0,
            'regularization_pending' => 0,
        ];

        foreach ($calendarWeeks as $week) {
            foreach ($week as $cell) {
                if (! ($cell['in_month'] ?? false) || ($cell['is_future'] ?? false)) {
                    continue;
                }

                $visualStatus = (string) ($cell['visual_status'] ?? '');
                match ($visualStatus) {
                    'present', 'early_out', 'in_progress' => $stats['present']++,
                    'late' => $stats['late']++,
                    'missing_punch_out' => $stats['missing_punch_out']++,
                    'absent' => $stats['absent']++,
                    'leave' => $stats['on_leave']++,
                    default => null,
                };

                if (($cell['regularization_status'] ?? null) === AttendanceRegularization::STATUS_PENDING) {
                    $stats['regularization_pending']++;
                }
            }
        }

        return $stats;
    }

    /**
     * @return array{year: int, month: int, employee_id?: int}
     */
    protected function attendanceIndexParams(string $date, ?int $employeeId = null): array
    {
        $parsed = Carbon::parse($date);
        $params = [
            'year' => $parsed->year,
            'month' => $parsed->month,
        ];
        if ($employeeId) {
            $params['employee_id'] = $employeeId;
        }

        return $params;
    }
}
