<?php

namespace App\Services;

use App\Models\AttendanceCorrection;
use App\Models\AttendanceRecord;
use App\Models\AuditLog;
use App\Models\Employee;
use App\Models\Shift;
use App\Models\ShiftAssignment;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class AttendanceService
{
    public function __construct(protected ErpNotificationService $notifications) {}

    public function record(Employee $employee, array $data, User $actor): AttendanceRecord
    {
        return DB::transaction(function () use ($employee, $data, $actor) {
            $date = Carbon::parse($data['attendance_date'])->startOfDay();
            $shift = isset($data['shift_id']) ? Shift::find($data['shift_id']) : $this->resolveShift($employee, $date);
            $schedule = $this->scheduleFor($shift, $date);
            $metrics = $this->calculateMetrics(
                $schedule['in'],
                $schedule['out'],
                isset($data['actual_check_in']) ? Carbon::parse($data['actual_check_in']) : null,
                isset($data['actual_check_out']) ? Carbon::parse($data['actual_check_out']) : null,
                $shift,
            );

            $existing = AttendanceRecord::where('employee_id', $employee->id)->whereDate('attendance_date', $date)->lockForUpdate()->first();
            $before = $existing?->toArray();
            $attributes = [
                'branch_id' => $employee->branch_id,
                'shift_id' => $shift?->id,
                'scheduled_in' => $schedule['in'],
                'scheduled_out' => $schedule['out'],
                'actual_check_in' => $data['actual_check_in'] ?? null,
                'actual_check_out' => $data['actual_check_out'] ?? null,
                'status' => $data['status'],
                'source' => $data['source'] ?? 'manual',
                'notes' => $data['notes'] ?? null,
                'created_by' => $actor->id,
                'approved_by' => $actor->id,
                'approved_at' => now(),
            ] + $metrics;
            if ($existing) {
                $existing->fill($attributes)->save();
                $record = $existing;
            } else {
                $record = AttendanceRecord::create([
                    'tenant_id' => $employee->tenant_id,
                    'employee_id' => $employee->id,
                    'attendance_date' => $date->toDateString(),
                ] + $attributes);
            }

            AuditLog::record('attendance', $existing ? 'attendance.updated' : 'attendance.created', $record, $before, $record->toArray(), $actor);

            return $record;
        });
    }

    public function requestCorrection(AttendanceRecord $record, array $data, User $actor): AttendanceCorrection
    {
        if (AttendanceCorrection::where('attendance_record_id', $record->id)->where('status', 'requested')->exists()) {
            throw ValidationException::withMessages(['attendance' => 'A correction request is already pending for this attendance record.']);
        }

        $correction = AttendanceCorrection::create([
            'tenant_id' => $record->tenant_id,
            'attendance_record_id' => $record->id,
            'employee_id' => $record->employee_id,
            'original_check_in' => $record->actual_check_in,
            'original_check_out' => $record->actual_check_out,
            'requested_check_in' => $data['requested_check_in'] ?? $record->actual_check_in,
            'requested_check_out' => $data['requested_check_out'] ?? $record->actual_check_out,
            'requested_status' => $data['requested_status'] ?? $record->status,
            'reason' => $data['reason'],
            'status' => 'requested',
            'requested_by' => $actor->id,
        ]);

        AuditLog::record('attendance', 'attendance.correction_requested', $correction, null, $correction->toArray(), $actor);
        $this->notifications->notifyPermission(
            $record->tenant_id,
            'attendance.approve',
            'attendance',
            'Attendance correction requested',
            $record->employee->full_name.' requested an attendance correction for '.$record->attendance_date->format('d M Y').'.',
            route('app.attendance.index', ['date' => $record->attendance_date->toDateString()]),
            $actor->id,
        );

        return $correction;
    }

    public function reviewCorrection(AttendanceCorrection $correction, string $decision, ?string $comment, User $actor): AttendanceCorrection
    {
        if ($correction->status !== 'requested') {
            throw ValidationException::withMessages(['attendance' => 'This correction request has already been reviewed.']);
        }

        return DB::transaction(function () use ($correction, $decision, $comment, $actor) {
            $record = AttendanceRecord::whereKey($correction->attendance_record_id)->lockForUpdate()->firstOrFail();
            $before = $record->toArray();

            if ($decision === 'approved') {
                $metrics = $this->calculateMetrics(
                    $record->scheduled_in,
                    $record->scheduled_out,
                    $correction->requested_check_in,
                    $correction->requested_check_out,
                    $record->shift,
                );
                $record->update([
                    'actual_check_in' => $correction->requested_check_in,
                    'actual_check_out' => $correction->requested_check_out,
                    'status' => $correction->requested_status ?: $record->status,
                    'approved_by' => $actor->id,
                    'approved_at' => now(),
                ] + $metrics);
            }

            $correction->update([
                'status' => $decision,
                'reviewed_by' => $actor->id,
                'reviewed_at' => now(),
                'review_comment' => $comment,
            ]);

            AuditLog::record('attendance', 'attendance.correction_'.$decision, $record, $before, $record->fresh()->toArray(), $actor);
            $this->notifications->notifyUser(
                $correction->requester,
                'attendance',
                'Attendance correction '.strtolower($decision),
                'Your correction request for '.$record->attendance_date->format('d M Y').' was '.strtolower($decision).'.',
                route('app.attendance.index', ['date' => $record->attendance_date->toDateString()]),
            );

            return $correction->fresh(['attendanceRecord', 'employee', 'reviewer']);
        });
    }

    public function resolveShift(Employee $employee, Carbon $date): ?Shift
    {
        $query = ShiftAssignment::with('shift')
            ->whereDate('effective_from', '<=', $date)
            ->where(fn ($builder) => $builder->whereNull('effective_to')->orWhereDate('effective_to', '>=', $date));

        $assignment = (clone $query)->where('employee_id', $employee->id)->latest('effective_from')->first()
            ?? (clone $query)->whereNull('employee_id')->where('department_id', $employee->department_id)->latest('effective_from')->first()
            ?? (clone $query)->whereNull('employee_id')->whereNull('department_id')->where('branch_id', $employee->branch_id)->latest('effective_from')->first();

        return $assignment?->shift;
    }

    public function calculateMetrics($scheduledIn, $scheduledOut, $actualIn, $actualOut, ?Shift $shift): array
    {
        $scheduledIn = $scheduledIn ? Carbon::parse($scheduledIn) : null;
        $scheduledOut = $scheduledOut ? Carbon::parse($scheduledOut) : null;
        $actualIn = $actualIn ? Carbon::parse($actualIn) : null;
        $actualOut = $actualOut ? Carbon::parse($actualOut) : null;
        $break = $shift?->break_minutes ?? 0;
        $worked = $actualIn && $actualOut ? max(0, $actualIn->diffInMinutes($actualOut, false) - $break) : 0;
        $grace = $shift?->grace_minutes ?? 0;
        $late = $scheduledIn && $actualIn ? max(0, $scheduledIn->diffInMinutes($actualIn, false) - $grace) : 0;
        $early = $scheduledOut && $actualOut ? max(0, $actualOut->diffInMinutes($scheduledOut, false)) : 0;
        $scheduledMinutes = $scheduledIn && $scheduledOut ? max(0, $scheduledIn->diffInMinutes($scheduledOut, false) - $break) : 0;
        $overtime = max(0, $worked - $scheduledMinutes);
        if ($overtime < ($shift?->minimum_overtime_minutes ?? 30)) {
            $overtime = 0;
        }

        return ['worked_minutes' => $worked, 'break_minutes' => $break, 'late_minutes' => $late, 'early_leave_minutes' => $early, 'overtime_minutes' => $overtime];
    }

    private function scheduleFor(?Shift $shift, Carbon $date): array
    {
        if (! $shift?->start_time || ! $shift?->end_time) {
            return ['in' => null, 'out' => null];
        }

        $start = Carbon::parse($date->toDateString().' '.$shift->start_time);
        $end = Carbon::parse($date->toDateString().' '.$shift->end_time);
        if ($end->lessThanOrEqualTo($start)) {
            $end->addDay();
        }

        return ['in' => $start, 'out' => $end];
    }
}
