<?php

namespace App\Services\Attendance;

use App\Models\Attendance;
use App\Models\Holiday;
use App\Models\User;
use App\Models\WorkDuration;
use Carbon\Carbon;
use InvalidArgumentException;

class AttendanceRecordWriter
{
    public const STATUS_PRESENT = 'present';

    public const STATUS_ABSENT = 'absent';

    public const STATUS_HALF_DAY = 'half_day';

    public const STATUS_LEAVE = 'leave';

    /**
     * @return list<string>
     */
    public static function allowedStatuses(): array
    {
        return [
            self::STATUS_PRESENT,
            self::STATUS_ABSENT,
            self::STATUS_HALF_DAY,
            self::STATUS_LEAVE,
        ];
    }

    public function __construct(
        protected WeekoffResolver $weekoffResolver,
        protected AttendanceCalendarDayResolver $calendarDayResolver
    ) {}

    public function isWeekoffDay(int $companyId, int $userId, Carbon $date): bool
    {
        return $this->weekoffResolver->isWeekoff($companyId, $userId, $date);
    }

    public function isHolidayDay(int $companyId, Carbon $date): bool
    {
        if ($companyId <= 0) {
            return false;
        }

        return Holiday::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereDate('date', $date->toDateString())
            ->exists();
    }

    /**
     * @return array{ok: bool, reason?: string}
     */
    public function assertWritableDay(int $companyId, int $userId, string $date): array
    {
        $dateObj = Carbon::parse($date)->startOfDay();
        if ($dateObj->isAfter(Carbon::today())) {
            return ['ok' => false, 'reason' => 'Future attendance dates are not allowed.'];
        }
        if ($this->isWeekoffDay($companyId, $userId, $dateObj)) {
            return ['ok' => false, 'reason' => 'Week off cannot be modified.'];
        }
        if ($this->isHolidayDay($companyId, $dateObj)) {
            return ['ok' => false, 'reason' => 'Holiday cannot be modified.'];
        }

        return ['ok' => true];
    }

    public function hasClockInDuplicate(int $companyId, int $userId, string $date): bool
    {
        return Attendance::query()
            ->where('company_id', $companyId)
            ->where('user_id', $userId)
            ->where('date', $date)
            ->whereNotNull('clock_in_date_time')
            ->exists();
    }

    /**
     * Apply imported attendance for a status. Returns 1 if a change was applied, 0 if skipped.
     *
     * @param  array{clock_in_time?: string|null, clock_out_time?: string|null}  $times
     */
    public function applyStatus(
        int $companyId,
        int $userId,
        string $date,
        string $status,
        array $times = [],
        bool $overwrite = false
    ): int {
        $status = strtolower(trim($status));
        if (! in_array($status, self::allowedStatuses(), true)) {
            throw new InvalidArgumentException('Invalid attendance status: '.$status);
        }

        $writable = $this->assertWritableDay($companyId, $userId, $date);
        if (! $writable['ok']) {
            return 0;
        }

        return match ($status) {
            self::STATUS_ABSENT => $this->markAbsent($companyId, $userId, $date),
            self::STATUS_LEAVE => $this->markLeave($companyId, $userId, $date),
            self::STATUS_PRESENT, self::STATUS_HALF_DAY => $this->upsertAttendanceForDate(
                $companyId,
                $userId,
                $date,
                [
                    'clock_in_time' => $times['clock_in_time'] ?? null,
                    'clock_out_time' => $times['clock_out_time'] ?? null,
                    'is_half_day' => $status === self::STATUS_HALF_DAY,
                    'is_late' => false,
                ],
                $overwrite,
                true
            ),
            default => 0,
        };
    }

    /**
     * @param  array{
     *   clock_in_time: string|null,
     *   clock_out_time?: string|null,
     *   clock_in_ip_address?: string|null,
     *   clock_out_ip_address?: string|null,
     *   is_late?: bool,
     *   is_half_day?: bool
     * }  $payload
     */
    public function upsertAttendanceForDate(
        int $companyId,
        int $userId,
        string $date,
        array $payload,
        bool $overwrite,
        bool $computeLateFromShift = false
    ): 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;
        }

        if (empty($payload['clock_in_time'])) {
            throw new InvalidArgumentException('clock_in_time is required.');
        }

        $clockIn = Carbon::parse($date.' '.$payload['clock_in_time']);
        $clockOut = ! empty($payload['clock_out_time'])
            ? Carbon::parse($date.' '.$payload['clock_out_time'])
            : null;

        $isLate = (bool) ($payload['is_late'] ?? false);
        if ($computeLateFromShift) {
            $user = User::query()->with('shift')->find($userId);
            $isLate = $user
                ? $this->calendarDayResolver->isLateArrivalForUser($user, $clockIn)
                : false;
        }

        $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' => $isLate,
            'is_half_day' => (bool) ($payload['is_half_day'] ?? false),
            'status' => self::STATUS_PRESENT,
            'is_leave' => false,
        ];

        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();
        }

        $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;
    }

    public function markAbsent(int $companyId, int $userId, string $date): int
    {
        $existing = Attendance::query()
            ->where('company_id', $companyId)
            ->where('user_id', $userId)
            ->where('date', $date)
            ->first();

        if (! $existing) {
            return 1;
        }

        WorkDuration::query()
            ->where('company_id', $companyId)
            ->where('attendance_id', $existing->id)
            ->delete();
        $existing->delete();

        return 1;
    }

    public function markLeave(int $companyId, int $userId, string $date): int
    {
        $attendance = Attendance::updateOrCreate(
            ['company_id' => $companyId, 'user_id' => $userId, 'date' => $date],
            [
                'status' => self::STATUS_LEAVE,
                'is_leave' => true,
                'is_half_day' => false,
                'is_late' => 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();

        return 1;
    }
}
