<?php

namespace Modules\Attendance\Services;

use App\Models\ActivityLogModel;
use App\Models\AttendanceRecordModel;
use App\Models\AttendanceShiftModel;
use App\Models\EmployeeModel;
use App\Models\EmployeeShiftAssignmentModel;
use App\Models\HolidayModel;
use App\Models\LeaveRequestModel;

class AttendanceService
{
    protected AttendanceRecordModel $records;
    protected AttendanceShiftModel $shifts;
    protected EmployeeShiftAssignmentModel $assignments;
    protected EmployeeModel $employees;
    protected HolidayModel $holidays;
    protected LeaveRequestModel $leaveRequests;
    protected ActivityLogModel $activity;

    public function __construct()
    {
        $this->records       = new AttendanceRecordModel();
        $this->shifts        = new AttendanceShiftModel();
        $this->assignments   = new EmployeeShiftAssignmentModel();
        $this->employees     = new EmployeeModel();
        $this->holidays      = new HolidayModel();
        $this->leaveRequests = new LeaveRequestModel();
        $this->activity      = new ActivityLogModel();
    }

    public function clockIn(int $employeeId, ?int $actingUserId): array
    {
        $employee = $this->employees->find($employeeId);
        if (! $employee || $employee['status'] !== 'active') {
            return ['success' => false, 'errors' => ['employee_id' => 'Employee not found or inactive.']];
        }

        $today = date('Y-m-d');
        $now   = date('Y-m-d H:i:s');

        if ($this->isHoliday($today)) {
            return ['success' => false, 'errors' => ['date' => 'Today is a company holiday.']];
        }

        if ($this->isOnLeave($employeeId, $today)) {
            return ['success' => false, 'errors' => ['employee_id' => 'Employee is on approved leave today.']];
        }

        $existing = $this->records->findForEmployeeOnDate($employeeId, $today);
        if ($existing && $existing['clock_in']) {
            return ['success' => false, 'errors' => ['clock_in' => 'Employee already clocked in today.']];
        }

        $shift = $this->assignments->getShiftForEmployeeOnDate($employeeId, $today);
        $shiftId = $shift ? (int) $shift['shift_id'] : null;

        $data = [
            'employee_id'     => $employeeId,
            'shift_id'        => $shiftId,
            'attendance_date' => $today,
            'clock_in'        => $now,
            'source'          => 'clock',
            'created_by'      => $actingUserId,
            'updated_by'      => $actingUserId,
        ];

        if ($existing) {
            $data = $this->applyMetrics(array_merge($existing, $data), $shift);
            $this->records->update($existing['id'], $data);
            $id = $existing['id'];
        } else {
            $data = $this->applyMetrics($data, $shift);
            $this->records->insert($data, true);
            $id = $this->records->getInsertID();
        }

        $this->activity->log('attendance', 'clock_in', $actingUserId, "Clock in for employee #{$employeeId} on {$today}", (string) $id);

        return ['success' => true, 'id' => $id];
    }

    public function clockOut(int $employeeId, ?int $actingUserId): array
    {
        $today    = date('Y-m-d');
        $now      = date('Y-m-d H:i:s');
        $existing = $this->records->findForEmployeeOnDate($employeeId, $today);

        if (! $existing || ! $existing['clock_in']) {
            return ['success' => false, 'errors' => ['clock_out' => 'Employee has not clocked in today.']];
        }

        if ($existing['clock_out']) {
            return ['success' => false, 'errors' => ['clock_out' => 'Employee already clocked out today.']];
        }

        $shift = $this->assignments->getShiftForEmployeeOnDate($employeeId, $today);
        $data  = array_merge($existing, [
            'clock_out'  => $now,
            'updated_by' => $actingUserId,
        ]);

        $data = $this->applyMetrics($data, $shift);
        $this->records->update($existing['id'], $data);

        $this->activity->log('attendance', 'clock_out', $actingUserId, "Clock out for employee #{$employeeId} on {$today}", (string) $existing['id']);

        return ['success' => true, 'id' => $existing['id']];
    }

    public function recordManual(array $input, ?int $actingUserId): array
    {
        $employeeId = (int) ($input['employee_id'] ?? 0);
        $date       = trim($input['attendance_date'] ?? '');
        $employee   = $this->employees->find($employeeId);

        if (! $employee) {
            return ['success' => false, 'errors' => ['employee_id' => 'Employee not found.']];
        }

        if ($date === '') {
            return ['success' => false, 'errors' => ['attendance_date' => 'Date is required.']];
        }

        $shiftId = ! empty($input['shift_id']) ? (int) $input['shift_id'] : null;
        $shift   = $shiftId
            ? $this->shifts->find($shiftId)
            : $this->assignments->getShiftForEmployeeOnDate($employeeId, $date);

        if ($shift && ! $shiftId) {
            $shiftId = (int) ($shift['shift_id'] ?? $shift['id']);
        }

        $clockIn  = $this->combineDateTime($date, $input['clock_in'] ?? null);
        $clockOut = $this->combineDateTime($date, $input['clock_out'] ?? null);

        if ($clockIn && $clockOut && strtotime($clockOut) <= strtotime($clockIn)) {
            return ['success' => false, 'errors' => ['clock_out' => 'Clock out must be after clock in.']];
        }

        $existing = $this->records->findForEmployeeOnDate($employeeId, $date);
        $data = [
            'employee_id'     => $employeeId,
            'shift_id'        => $shiftId,
            'attendance_date' => $date,
            'clock_in'        => $clockIn,
            'clock_out'       => $clockOut,
            'status'          => $input['status'] ?? 'present',
            'source'          => 'manual',
            'notes'           => trim($input['notes'] ?? '') ?: null,
            'updated_by'      => $actingUserId,
        ];

        $data = $this->applyMetrics($data, is_array($shift) ? $shift : null);

        if ($existing) {
            $this->records->update($existing['id'], $data);
            $id = $existing['id'];
            $action = 'manual_update';
        } else {
            $data['created_by'] = $actingUserId;
            $this->records->insert($data, true);
            $id = $this->records->getInsertID();
            $action = 'manual_create';
        }

        $this->activity->log('attendance', $action, $actingUserId, "Manual attendance for employee #{$employeeId} on {$date}", (string) $id);

        return ['success' => true, 'id' => $id];
    }

    public function applyMetrics(array $record, ?array $shift): array
    {
        $date     = $record['attendance_date'];
        $clockIn  = $record['clock_in'] ?? null;
        $clockOut = $record['clock_out'] ?? null;

        $record['late_minutes']       = 0;
        $record['early_exit_minutes'] = 0;
        $record['overtime_minutes']   = 0;
        $record['work_minutes']       = 0;

        if ($clockIn && $clockOut) {
            $record['work_minutes'] = max(0, (int) round((strtotime($clockOut) - strtotime($clockIn)) / 60));
            if ($shift) {
                $break = (int) ($shift['break_minutes'] ?? 0);
                $record['work_minutes'] = max(0, $record['work_minutes'] - $break);
            }
        }

        if ($shift && $clockIn) {
            $shiftStart = strtotime("{$date} {$shift['start_time']}");
            $grace      = ((int) ($shift['grace_period_minutes'] ?? 0)) * 60;
            $lateSecs   = strtotime($clockIn) - ($shiftStart + $grace);
            if ($lateSecs > 0) {
                $record['late_minutes'] = (int) ceil($lateSecs / 60);
            }
        }

        if ($shift && $clockOut) {
            $shiftEnd   = strtotime("{$date} {$shift['end_time']}");
            $earlyGrace = ((int) ($shift['early_exit_grace_minutes'] ?? 0)) * 60;
            $earlySecs  = ($shiftEnd - $earlyGrace) - strtotime($clockOut);
            if ($earlySecs > 0) {
                $record['early_exit_minutes'] = (int) ceil($earlySecs / 60);
            }

            $otThreshold = ((int) ($shift['overtime_after_minutes'] ?? 0)) * 60;
            $otSecs      = strtotime($clockOut) - ($shiftEnd + $otThreshold);
            if ($otSecs > 0) {
                $record['overtime_minutes'] = (int) floor($otSecs / 60);
            }
        }

        if (! empty($record['status']) && ! in_array($record['status'], ['on_leave', 'holiday', 'absent'], true)) {
            if ($record['late_minutes'] > 0) {
                $record['status'] = 'late';
            } elseif ($clockIn && $clockOut && $record['work_minutes'] > 0 && $record['work_minutes'] < 240) {
                $record['status'] = 'half_day';
            } elseif ($clockIn) {
                $record['status'] = $record['source'] === 'auto' ? 'auto_present' : 'present';
            }
        }

        return $record;
    }

    public function getTodayBoard(string $date, ?string $search = null, int $perPage = 50): array
    {
        $employees = $this->employees
            ->scopeForList($search, 'active')
            ->orderBy('full_name', 'ASC')
            ->paginate($perPage);

        $employeeIds = array_map(static fn (array $row): int => (int) $row['id'], $employees);
        $shiftMap    = $this->assignments->getShiftsForEmployeesOnDate($employeeIds, $date);
        $leaveIds    = array_flip($this->getOnLeaveEmployeeIds($employeeIds, $date));
        $isHoliday   = $this->isHoliday($date);

        $records = $this->records->getForDateWithEmployees($date, $search);
        $recordMap = [];
        foreach ($records as $r) {
            $recordMap[$r['employee_id']] = $r;
        }

        $board = [];
        foreach ($employees as $emp) {
            $employeeId = (int) $emp['id'];
            $board[] = [
                'employee'   => $emp,
                'record'     => $recordMap[$employeeId] ?? null,
                'shift'      => $shiftMap[$employeeId] ?? null,
                'on_leave'   => isset($leaveIds[$employeeId]),
                'is_holiday' => $isHoliday,
            ];
        }

        return [
            'board' => $board,
            'pager' => $this->employees->pager,
        ];
    }

    /**
     * @return array{total:int,present:int,absent:int,late:int,on_leave:int,holiday:int,clocked:int}
     */
    public function getTodayStats(string $date, ?string $search = null): array
    {
        $isHoliday = $this->isHoliday($date);
        $total     = $this->employees->scopeForList($search, 'active')->countAllResults(false);

        $onLeave = 0;
        if ($total > 0) {
            $leaveBuilder = $this->leaveRequests
                ->select('leave_requests.employee_id')
                ->join('employees e', 'e.id = leave_requests.employee_id')
                ->where('leave_requests.status', 'approved')
                ->where('leave_requests.start_date <=', $date)
                ->where('leave_requests.end_date >=', $date)
                ->where('e.status', 'active')
                ->where('e.deleted_at', null);

            if ($search) {
                $leaveBuilder->groupStart()
                    ->like('e.full_name', $search)
                    ->orLike('e.employee_number', $search)
                    ->groupEnd();
            }

            $onLeave = $leaveBuilder->countAllResults(false);
        }

        $stats = [
            'total'    => $total,
            'present'  => 0,
            'absent'   => 0,
            'late'     => 0,
            'on_leave' => $onLeave,
            'holiday'  => $isHoliday ? $total : 0,
            'clocked'  => 0,
        ];

        if ($isHoliday || $total === 0) {
            return $stats;
        }

        $recordBuilder = $this->records->db->table('attendance_records ar')
            ->select('ar.status, ar.clock_in')
            ->join('employees e', 'e.id = ar.employee_id')
            ->where('ar.attendance_date', $date)
            ->where('e.status', 'active')
            ->where('e.deleted_at', null);

        if ($search) {
            $recordBuilder->groupStart()
                ->like('e.full_name', $search)
                ->orLike('e.employee_number', $search)
                ->groupEnd();
        }

        foreach ($recordBuilder->get()->getResultArray() as $row) {
            $status = $row['status'] ?? '';
            if (in_array($status, ['present', 'auto_present'], true)) {
                $stats['present']++;
            } elseif ($status === 'late') {
                $stats['late']++;
                $stats['present']++;
            } elseif ($status === 'absent') {
                $stats['absent']++;
            }

            if (! empty($row['clock_in'])) {
                $stats['clocked']++;
            }
        }

        $tracked = $stats['present'] + $stats['absent'];
        $stats['absent'] += max(0, ($total - $onLeave) - $tracked);

        return $stats;
    }

    /**
     * @param list<int|string> $employeeIds
     *
     * @return list<int>
     */
    protected function getOnLeaveEmployeeIds(array $employeeIds, string $date): array
    {
        $employeeIds = array_values(array_filter(array_map('intval', $employeeIds)));
        if ($employeeIds === []) {
            return [];
        }

        $rows = $this->leaveRequests
            ->select('employee_id')
            ->whereIn('employee_id', $employeeIds)
            ->where('status', 'approved')
            ->where('start_date <=', $date)
            ->where('end_date >=', $date)
            ->findAll();

        return array_map(static fn (array $row): int => (int) $row['employee_id'], $rows);
    }

    public function isHoliday(string $date): bool
    {
        return $this->holidays->where('date', $date)->countAllResults() > 0;
    }

    public function isOnLeave(int $employeeId, string $date): bool
    {
        return $this->leaveRequests
            ->where('employee_id', $employeeId)
            ->where('status', 'approved')
            ->where('start_date <=', $date)
            ->where('end_date >=', $date)
            ->countAllResults() > 0;
    }

    protected function combineDateTime(string $date, ?string $time): ?string
    {
        if (! $time || trim($time) === '') {
            return null;
        }

        return date('Y-m-d H:i:s', strtotime("{$date} {$time}"));
    }
}
