<?php

namespace App\Services;

use App\Models\AttendanceImport;
use App\Models\AttendanceImportRow;
use App\Models\User;
use App\Notifications\AttendanceImportCompletedNotification;
use App\Services\Attendance\AttendanceRecordWriter;
use App\Support\ResolvesPrivateUploads;
use Carbon\Carbon;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Validator;
use Throwable;

class AttendanceImportService
{
    use ResolvesPrivateUploads;

    public const SYNC_ROW_THRESHOLD = 50;

    public function __construct(
        protected AttendanceRecordWriter $attendanceRecordWriter
    ) {}

    /**
     * @return list<string>
     */
    public function templateHeaders(): array
    {
        return [
            'employee_code',
            'attendance_date',
            'check_in_time',
            'check_out_time',
            'attendance_status',
        ];
    }

    /**
     * @return list<string>
     */
    public function templateSampleRow(): array
    {
        return [
            'EMP00001',
            now()->subDay()->toDateString(),
            '09:00',
            '18:00',
            'present',
        ];
    }

    public function createAndValidatePreview(UploadedFile $file, User $actor): AttendanceImport
    {
        $companyId = (int) $actor->company_id;
        $path = $file->store('imports/attendance/'.($companyId ?: 'shared'), $this->privateUploadDisk());
        $rows = $this->readCsvRows($path);
        $mode = count($rows) <= self::SYNC_ROW_THRESHOLD ? 'sync' : 'queued';

        $import = AttendanceImport::query()->create([
            'company_id' => $companyId,
            'created_by' => $actor->id,
            'status' => AttendanceImport::STATUS_PREVIEW,
            'mode' => $mode,
            'file_path' => $path,
            'original_file_name' => $file->getClientOriginalName(),
            'total_rows' => count($rows),
            'summary_json' => [
                'headers' => $this->templateHeaders(),
            ],
        ]);

        $this->buildPreviewRows($import, $actor, $rows);

        return $import->fresh(['rows']);
    }

    /**
     * @param  array<int, array<string, string>>  $rows
     */
    public function buildPreviewRows(AttendanceImport $import, User $actor, ?array $rows = null): AttendanceImport
    {
        $rows ??= $this->readCsvRows((string) $import->file_path);
        $companyId = (int) $import->company_id;
        $visibleIds = array_map('intval', $actor->visibleStaffUserIds());
        $seenKeys = [];

        $valid = 0;
        $invalid = 0;
        $duplicate = 0;

        AttendanceImportRow::query()
            ->where('attendance_import_id', $import->id)
            ->delete();

        foreach ($rows as $rowNumber => $row) {
            $normalized = $this->normalizeRow($row);
            [$status, $message, $userId, $fields] = $this->classifyRow(
                $normalized,
                $companyId,
                $visibleIds,
                $seenKeys
            );

            if ($status === AttendanceImportRow::STATUS_VALID && $userId && ! empty($fields['attendance_date'])) {
                $seenKeys[$userId.'|'.$fields['attendance_date']] = true;
            }

            AttendanceImportRow::query()->create([
                'attendance_import_id' => $import->id,
                'row_number' => $rowNumber,
                'status' => $status,
                'employee_code' => $fields['employee_code'] ?? null,
                'user_id' => $userId,
                'attendance_date' => $fields['attendance_date'] ?? null,
                'check_in_time' => $fields['check_in_time'] ?? null,
                'check_out_time' => $fields['check_out_time'] ?? null,
                'attendance_status' => $fields['attendance_status'] ?? null,
                'error_message' => $message,
                'payload_json' => $normalized,
            ]);

            match ($status) {
                AttendanceImportRow::STATUS_VALID => $valid++,
                AttendanceImportRow::STATUS_DUPLICATE => $duplicate++,
                default => $invalid++,
            };
        }

        $import->update([
            'total_rows' => count($rows),
            'valid_rows' => $valid,
            'invalid_rows' => $invalid,
            'duplicate_rows' => $duplicate,
            'status' => AttendanceImport::STATUS_PREVIEW,
        ]);

        return $import->fresh();
    }

    public function confirm(AttendanceImport $import): AttendanceImport
    {
        if ($import->status !== AttendanceImport::STATUS_PREVIEW) {
            return $import;
        }

        $import->update([
            'status' => AttendanceImport::STATUS_PROCESSING,
            'started_at' => now(),
            'processed_rows' => 0,
            'success_rows' => 0,
            'failed_rows' => 0,
        ]);

        $success = 0;
        $failed = 0;
        $processed = 0;

        $rows = AttendanceImportRow::query()
            ->where('attendance_import_id', $import->id)
            ->where('status', AttendanceImportRow::STATUS_VALID)
            ->orderBy('row_number')
            ->get();

        foreach ($rows as $row) {
            $processed++;
            try {
                $applied = $this->attendanceRecordWriter->applyStatus(
                    (int) $import->company_id,
                    (int) $row->user_id,
                    Carbon::parse($row->attendance_date)->toDateString(),
                    (string) $row->attendance_status,
                    [
                        'clock_in_time' => $row->check_in_time,
                        'clock_out_time' => $row->check_out_time,
                    ],
                    false
                );

                if ($applied === 1) {
                    $row->update([
                        'status' => AttendanceImportRow::STATUS_IMPORTED,
                        'error_message' => null,
                    ]);
                    $success++;
                } else {
                    $row->update([
                        'status' => AttendanceImportRow::STATUS_FAILED,
                        'error_message' => 'Attendance could not be applied (week off, holiday, or duplicate).',
                    ]);
                    $failed++;
                }
            } catch (Throwable $e) {
                $row->update([
                    'status' => AttendanceImportRow::STATUS_FAILED,
                    'error_message' => $e->getMessage(),
                ]);
                $failed++;
            }
        }

        $import->update([
            'status' => AttendanceImport::STATUS_COMPLETED,
            'processed_rows' => $processed,
            'success_rows' => $success,
            'failed_rows' => (int) $import->invalid_rows + $failed,
            'completed_at' => now(),
            'summary_json' => array_merge($import->summary_json ?? [], [
                'total_records' => (int) $import->total_rows,
                'successfully_imported' => $success,
                'duplicate_records' => (int) $import->duplicate_rows,
                'failed_records' => (int) $import->invalid_rows + $failed,
                'apply_failures' => $failed,
            ]),
        ]);

        return $import->fresh(['rows']);
    }

    public function processDeferred(int $importId): void
    {
        $import = AttendanceImport::query()->find($importId);
        if (! $import) {
            return;
        }

        $actor = User::query()->find($import->created_by);

        try {
            $import = $this->confirm($import);
            if ($actor) {
                $actor->notify(new AttendanceImportCompletedNotification($import, true));
            }
        } catch (Throwable $e) {
            $import->update([
                'status' => AttendanceImport::STATUS_FAILED,
                'completed_at' => now(),
                'summary_json' => array_merge($import->summary_json ?? [], [
                    'error' => $e->getMessage(),
                ]),
            ]);
            if ($actor) {
                $actor->notify(new AttendanceImportCompletedNotification($import->fresh(), false, $e->getMessage()));
            }
        }
    }

    public function cancel(AttendanceImport $import): AttendanceImport
    {
        if ($import->status !== AttendanceImport::STATUS_PREVIEW) {
            return $import;
        }

        $import->update([
            'status' => AttendanceImport::STATUS_CANCELLED,
            'completed_at' => now(),
        ]);

        return $import->fresh();
    }

    /**
     * @return list<list<string|null>>
     */
    public function errorReportRows(AttendanceImport $import): array
    {
        $headers = [
            'row_number',
            'employee_code',
            'attendance_date',
            'check_in_time',
            'check_out_time',
            'attendance_status',
            'status',
            'error_message',
        ];

        $lines = [$headers];
        $rows = AttendanceImportRow::query()
            ->where('attendance_import_id', $import->id)
            ->whereIn('status', [
                AttendanceImportRow::STATUS_INVALID,
                AttendanceImportRow::STATUS_DUPLICATE,
                AttendanceImportRow::STATUS_FAILED,
            ])
            ->orderBy('row_number')
            ->get();

        foreach ($rows as $row) {
            $lines[] = [
                (string) $row->row_number,
                (string) ($row->employee_code ?? ''),
                $row->attendance_date?->toDateString() ?? '',
                (string) ($row->check_in_time ?? ''),
                (string) ($row->check_out_time ?? ''),
                (string) ($row->attendance_status ?? ''),
                (string) $row->status,
                (string) ($row->error_message ?? ''),
            ];
        }

        return $lines;
    }

    /**
     * @param  array<string, mixed>  $row
     * @param  list<int>  $visibleIds
     * @param  array<string, bool>  $seenKeys
     * @return array{0: string, 1: string|null, 2: int|null, 3: array<string, mixed>}
     */
    private function classifyRow(array $row, int $companyId, array $visibleIds, array $seenKeys): array
    {
        $fields = [
            'employee_code' => $row['employee_code'] ?? null,
            'attendance_date' => $row['attendance_date'] ?? null,
            'check_in_time' => $row['check_in_time'] ?? null,
            'check_out_time' => $row['check_out_time'] ?? null,
            'attendance_status' => isset($row['attendance_status'])
                ? strtolower(trim((string) $row['attendance_status']))
                : null,
        ];

        $rules = [
            'employee_code' => ['required', 'string', 'max:100'],
            'attendance_date' => ['required', 'date_format:Y-m-d'],
            'attendance_status' => ['required', 'in:'.implode(',', AttendanceRecordWriter::allowedStatuses())],
            'check_in_time' => ['nullable', 'regex:/^\d{1,2}:\d{2}(:\d{2})?$/'],
            'check_out_time' => ['nullable', 'regex:/^\d{1,2}:\d{2}(:\d{2})?$/'],
        ];

        $status = $fields['attendance_status'];
        if (in_array($status, [AttendanceRecordWriter::STATUS_PRESENT, AttendanceRecordWriter::STATUS_HALF_DAY], true)) {
            $rules['check_in_time'] = ['required', 'regex:/^\d{1,2}:\d{2}(:\d{2})?$/'];
            $rules['check_out_time'] = ['required', 'regex:/^\d{1,2}:\d{2}(:\d{2})?$/'];
        }

        $validator = Validator::make($fields, $rules, [
            'employee_code.required' => 'Employee code is required.',
            'attendance_date.required' => 'Attendance date is required.',
            'attendance_date.date_format' => 'Attendance date must be Y-m-d.',
            'attendance_status.required' => 'Attendance status is required.',
            'attendance_status.in' => 'Attendance status must be present, absent, half_day, or leave.',
            'check_in_time.required' => 'Check-in time is required for present/half_day.',
            'check_out_time.required' => 'Check-out time is required for present/half_day.',
        ]);

        if ($validator->fails()) {
            return [
                AttendanceImportRow::STATUS_INVALID,
                $validator->errors()->first(),
                null,
                $fields,
            ];
        }

        $date = Carbon::parse((string) $fields['attendance_date'])->toDateString();
        $fields['attendance_date'] = $date;
        $fields['check_in_time'] = $this->normalizeTime($fields['check_in_time'] ?? null);
        $fields['check_out_time'] = $this->normalizeTime($fields['check_out_time'] ?? null);

        if (
            in_array($status, [AttendanceRecordWriter::STATUS_PRESENT, AttendanceRecordWriter::STATUS_HALF_DAY], true)
            && $fields['check_in_time']
            && $fields['check_out_time']
        ) {
            $in = Carbon::parse($date.' '.$fields['check_in_time']);
            $out = Carbon::parse($date.' '.$fields['check_out_time']);
            if ($out->lte($in)) {
                return [
                    AttendanceImportRow::STATUS_INVALID,
                    'Check-out time must be after check-in time.',
                    null,
                    $fields,
                ];
            }
        }

        $user = $this->resolveEmployee($companyId, (string) $fields['employee_code']);
        if (! $user) {
            return [
                AttendanceImportRow::STATUS_INVALID,
                'Employee not found for code: '.$fields['employee_code'],
                null,
                $fields,
            ];
        }

        if (! in_array((int) $user->id, $visibleIds, true)) {
            return [
                AttendanceImportRow::STATUS_INVALID,
                'You cannot import attendance for this employee.',
                (int) $user->id,
                $fields,
            ];
        }

        $writable = $this->attendanceRecordWriter->assertWritableDay($companyId, (int) $user->id, $date);
        if (! $writable['ok']) {
            return [
                AttendanceImportRow::STATUS_INVALID,
                $writable['reason'] ?? 'Date is not writable.',
                (int) $user->id,
                $fields,
            ];
        }

        $key = $user->id.'|'.$date;
        if (isset($seenKeys[$key])) {
            return [
                AttendanceImportRow::STATUS_DUPLICATE,
                'Duplicate employee and date within the import file.',
                (int) $user->id,
                $fields,
            ];
        }

        if ($this->attendanceRecordWriter->hasClockInDuplicate($companyId, (int) $user->id, $date)) {
            return [
                AttendanceImportRow::STATUS_DUPLICATE,
                'Attendance already exists for this employee and date.',
                (int) $user->id,
                $fields,
            ];
        }

        return [
            AttendanceImportRow::STATUS_VALID,
            null,
            (int) $user->id,
            $fields,
        ];
    }

    private function resolveEmployee(int $companyId, string $empCode): ?User
    {
        $empCode = trim($empCode);
        if ($empCode === '' || $companyId <= 0) {
            return null;
        }

        $base = User::query()
            ->where('company_id', $companyId)
            ->where('user_type', User::USER_TYPE_STAFF)
            ->where('status', User::STATUS_ACTIVE);

        $exact = (clone $base)->where('employee_number', $empCode)->first();
        if ($exact) {
            return $exact;
        }

        if (! ctype_digit($empCode)) {
            return null;
        }

        $normalizedCode = ltrim($empCode, '0');
        if ($normalizedCode === '') {
            $normalizedCode = '0';
        }

        $candidates = (clone $base)
            ->whereNotNull('employee_number')
            ->where('employee_number', '!=', '')
            ->get(['id', 'employee_number']);

        $matches = $candidates->filter(function (User $user) use ($normalizedCode) {
            $number = trim((string) $user->employee_number);
            if ($number === '' || ! ctype_digit($number)) {
                return false;
            }
            $normalizedNumber = ltrim($number, '0');
            if ($normalizedNumber === '') {
                $normalizedNumber = '0';
            }

            return $normalizedNumber === $normalizedCode;
        });

        if ($matches->count() === 1) {
            return User::query()->find($matches->first()->id);
        }

        return null;
    }

    private function normalizeTime(mixed $value): ?string
    {
        if ($value === null || $value === '') {
            return null;
        }

        $value = trim((string) $value);
        if (! preg_match('/^\d{1,2}:\d{2}(:\d{2})?$/', $value)) {
            return $value;
        }

        $parts = explode(':', $value);
        $h = str_pad((string) (int) $parts[0], 2, '0', STR_PAD_LEFT);
        $m = str_pad((string) (int) $parts[1], 2, '0', STR_PAD_LEFT);
        $s = isset($parts[2]) ? str_pad((string) (int) $parts[2], 2, '0', STR_PAD_LEFT) : '00';

        return $h.':'.$m.':'.$s;
    }

    /**
     * @param  array<string, mixed>  $row
     * @return array<string, mixed>
     */
    private function normalizeRow(array $row): array
    {
        $normalized = [];
        foreach ($this->templateHeaders() as $column) {
            $value = $row[$column] ?? null;
            if (is_string($value)) {
                $value = trim($value);
            }
            $normalized[$column] = $value === '' ? null : $value;
        }

        return $normalized;
    }

    /**
     * @return array<int, array<string, string>>
     */
    private function readCsvRows(string $path): array
    {
        if (! $this->privateUploadExists($path)) {
            return [];
        }

        $fullPath = $this->privateUploadAbsolutePath($path);
        $handle = fopen($fullPath, 'r');
        if ($handle === false) {
            return [];
        }

        $header = fgetcsv($handle) ?: [];
        $header = array_map(fn ($item) => trim((string) $item), $header);
        $rows = [];
        $index = 1;

        while (($line = fgetcsv($handle)) !== false) {
            $index++;
            if (! is_array($line)) {
                continue;
            }
            $assoc = [];
            foreach ($header as $offset => $column) {
                if ($column === '') {
                    continue;
                }
                $assoc[$column] = isset($line[$offset]) ? trim((string) $line[$offset]) : '';
            }

            if (array_filter($assoc, fn ($value) => $value !== '' && $value !== null) === []) {
                continue;
            }

            $rows[$index] = $assoc;
        }

        fclose($handle);

        return $rows;
    }
}
