<?php

namespace App\Services\Biometric;

use App\Models\AttendancePunch;
use App\Models\BiometricDevice;
use App\Models\Company;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;

class ZktecoPunchIngestService
{
    public function __construct(
        protected ZktecoPayloadParser $parser,
        protected PunchAttendanceApplicator $applicator,
        protected BiometricFirstInLastOutService $filoService,
    ) {}

    /**
     * @param  array<int, mixed>  $rows
     * @return array{accepted: int, duplicates: int, rejected: int, punch_ids: list<int>, reject_reasons: list<string>}
     */
    public function ingest(BiometricDevice $device, Company $company, array $rows, ?string $requestIp = null): array
    {
        $max = (int) config('zkteco.max_batch_size', 500);
        if (count($rows) > $max) {
            throw ValidationException::withMessages([
                'payload' => "Batch exceeds maximum of {$max} punches.",
            ]);
        }

        $accepted = 0;
        $duplicates = 0;
        $rejected = 0;
        $punchIds = [];
        $rejectReasons = [];

        $tz = $company->timezone ?: config('app.timezone', 'Asia/Kolkata');

        DB::transaction(function () use (
            $device,
            $company,
            $rows,
            $tz,
            &$accepted,
            &$duplicates,
            &$rejected,
            &$punchIds,
            &$rejectReasons
        ) {
            foreach ($rows as $index => $row) {
                if (! is_array($row)) {
                    $rejected++;
                    $rejectReasons[] = "row {$index}: not an object";
                    continue;
                }

                $row = $this->parser->normalizeRow($row);

                $empCode = trim((string) ($row['emp_code'] ?? ''));
                $punchDatetimeRaw = trim((string) ($row['punch_datetime'] ?? ''));
                $punchState = trim((string) ($row['punch_state'] ?? ''));
                $payloadSn = trim((string) ($row['terminal_sn'] ?? ''));

                if ($empCode === '' || $punchDatetimeRaw === '') {
                    $rejected++;
                    $rejectReasons[] = "row {$index}: missing emp_code or punch_datetime";
                    continue;
                }

                if ($payloadSn !== '' && strcasecmp($payloadSn, $device->terminal_sn) !== 0) {
                    $rejected++;
                    $rejectReasons[] = "row {$index}: terminal_sn mismatch (got {$payloadSn})";
                    continue;
                }

                $punchAt = $this->parser->parsePunchDateTime($punchDatetimeRaw, $tz);
                if (! $punchAt) {
                    $rejected++;
                    $rejectReasons[] = "row {$index}: invalid punch_datetime [{$punchDatetimeRaw}]";
                    continue;
                }

                $terminalSn = $payloadSn !== '' ? $payloadSn : $device->terminal_sn;
                $stateKey = $punchState !== '' ? $punchState : '';

                $exists = AttendancePunch::withoutGlobalScopes()
                    ->where('company_id', $company->id)
                    ->where('terminal_sn', $terminalSn)
                    ->where('emp_code', $empCode)
                    ->where('punch_datetime', $punchAt->format('Y-m-d H:i:s'))
                    ->where('punch_state', $stateKey)
                    ->exists();

                if ($exists) {
                    $duplicates++;
                    continue;
                }

                $punchDate = null;
                if (! empty($row['punch_date'])) {
                    $parsedDate = $this->parser->parsePunchDateTime(
                        trim((string) $row['punch_date']).' 00:00:00',
                        $tz
                    );
                    $punchDate = $parsedDate?->toDateString() ?? $punchAt->toDateString();
                } else {
                    $punchDate = $punchAt->toDateString();
                }

                $punchTime = ! empty($row['punch_time'])
                    ? (string) $row['punch_time']
                    : $punchAt->format('H:i:s');

                $deviceTxnId = $this->nullableString($row['device_transaction_id'] ?? null);

                if ($deviceTxnId !== null) {
                    $existsTxn = AttendancePunch::withoutGlobalScopes()
                        ->where('biometric_device_id', $device->id)
                        ->where('device_transaction_id', $deviceTxnId)
                        ->exists();
                    if ($existsTxn) {
                        $duplicates++;
                        continue;
                    }
                }

                try {
                    $punch = AttendancePunch::withoutGlobalScopes()->create([
                        'company_id' => $company->id,
                        'biometric_device_id' => $device->id,
                        'emp_code' => $empCode,
                        'punch_datetime' => $punchAt->format('Y-m-d H:i:s'),
                        'punch_date' => $punchDate,
                        'punch_time' => $punchTime,
                        'punch_state' => $stateKey,
                        'verify_type' => $this->nullableString($row['verify_type'] ?? null),
                        'terminal_sn' => $terminalSn,
                        'terminal_alias' => $this->nullableString($row['terminal_alias'] ?? $device->terminal_alias),
                        'longitude' => $this->nullableString($row['longitude'] ?? null),
                        'latitude' => $this->nullableString($row['latitude'] ?? null),
                        'gps_location' => $this->nullableString($row['gps_location'] ?? null),
                        'temperature' => $this->nullableString($row['temperature'] ?? null),
                        'mask_flag' => $this->nullableString($row['mask_flag'] ?? null),
                        'upload_time' => $this->nullableString($row['upload_time'] ?? null),
                        'device_transaction_id' => $deviceTxnId,
                        'source' => AttendancePunch::SOURCE_BIOMETRIC_DEVICE,
                        'raw_payload' => $row,
                        'status' => AttendancePunch::STATUS_PENDING,
                    ]);
                } catch (\Illuminate\Database\QueryException $e) {
                    $duplicates++;
                    Log::debug('zkteco.duplicate_or_db', ['message' => $e->getMessage()]);
                    continue;
                }

                $accepted++;
                $punchIds[] = (int) $punch->id;
            }

            $device->forceFill([
                'last_seen_at' => now(),
                'last_punch_at' => $accepted > 0 ? now() : $device->last_punch_at,
            ])->save();
        });

        if ($rejected > 0) {
            Log::warning('zkteco.ingest_rejected', [
                'device_id' => $device->id,
                'company_id' => $company->id,
                'rejected' => $rejected,
                'reasons' => array_slice($rejectReasons, 0, 20),
            ]);
        }

        // Apply chronologically (important for login/logout gate sessions and FILO day order).
        $orderedIds = $this->orderPunchIdsChronologically($punchIds);

        foreach ($orderedIds as $id) {
            $this->applyPunch((int) $id);
        }

        return [
            'accepted' => $accepted,
            'duplicates' => $duplicates,
            'rejected' => $rejected,
            'punch_ids' => $punchIds,
            'reject_reasons' => array_values(array_unique(array_slice($rejectReasons, 0, 10))),
        ];
    }

    public function applyPunch(int $punchId): void
    {
        $punch = AttendancePunch::withoutGlobalScopes()->find($punchId);
        if (! $punch) {
            return;
        }

        $device = BiometricDevice::withoutGlobalScopes()->find($punch->biometric_device_id);
        $mode = $device?->punch_mode ?: BiometricDevice::PUNCH_MODE_BOTH;
        $isFilo = $mode === BiometricDevice::PUNCH_MODE_BOTH;

        // Gate path skips already-applied; FILO processPunch recalculates the day from pending/new.
        if (! $isFilo && $punch->status === AttendancePunch::STATUS_APPLIED) {
            return;
        }

        try {
            if ($isFilo) {
                $this->filoService->processPunch($punch);
            } else {
                if ($punch->status === AttendancePunch::STATUS_APPLIED) {
                    return;
                }
                $this->applicator->apply($punch);
            }
        } catch (\Throwable $e) {
            Log::error('zkteco.apply_failed', [
                'punch_id' => $punchId,
                'emp_code' => $punch->emp_code,
                'message' => $e->getMessage(),
            ]);

            $fresh = AttendancePunch::withoutGlobalScopes()->find($punchId);
            if ($fresh && $fresh->status !== AttendancePunch::STATUS_APPLIED) {
                $fresh->forceFill([
                    'status' => AttendancePunch::STATUS_ERROR,
                    'error_message' => 'Apply failed: '.$e->getMessage(),
                    'processed_at' => now(),
                ])->save();
            }
        }
    }

    /**
     * @param  list<int>  $punchIds
     * @return list<int>
     */
    public function orderPunchIdsChronologically(array $punchIds): array
    {
        if ($punchIds === []) {
            return [];
        }

        return AttendancePunch::withoutGlobalScopes()
            ->whereIn('id', $punchIds)
            ->orderBy('punch_datetime')
            ->orderBy('id')
            ->pluck('id')
            ->map(fn ($id) => (int) $id)
            ->all();
    }

    protected function nullableString(mixed $value): ?string
    {
        if ($value === null) {
            return null;
        }
        $value = trim((string) $value);

        return $value === '' ? null : $value;
    }
}
