<?php

namespace App\Http\Controllers;

use App\Models\Attendance;
use App\Models\Device;
use App\Models\StudentFaceRegistration;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

/**
 * Endpoint ini dipanggil LANGSUNG oleh device (bukan Laravel yang manggil device).
 * Semua device dikonfigurasi (lewat FaceDeviceService::configureHttpHost()) buat
 * POST ke sini setiap ada event, biar realtime, gak perlu polling/scheduler lagi.
 *
 * PENTING - struktur JSON asli device (dikonfirmasi dari tes langsung ke ngrok,
 * BUKAN dari dokumentasi ISAPI - dokumentasinya menyesatkan soal ini):
 *
 * Field multipart bernama "AccessControllerEvent", isinya JSON kira-kira gini:
 * {
 *   "ipAddress": "192.168.8.5",        <- IP device pengirim, dipakai buat cari device_id
 *   "dateTime": "...",
 *   "eventType": "heartBeat" ATAU "AccessControllerEvent",
 *   "eventState": "active",
 *   "deviceID": "...",
 *   "AccessControllerEvent": {          <- NESTED, nama key-nya SAMA kayak field luar
 *       "majorEventType": 5,            <- BUKAN "major"
 *       "subEventType": 75,             <- BUKAN "minor" (75 = sukses, dikonfirmasi dari tes)
 *       "doorNo": 1,
 *       "employeeNoString": "S2251",    <- cuma ada kalau event terkait orang (bukan door open/close)
 *       "name": "...",
 *       "serialNo": 123
 *   }
 * }
 *
 * CATATAN DEBUG (2026-07-20): sempat ketemu kasus employeeNoString "1" (data
 * enrollment test lama di device, dateTime-nya jauh di masa lalu / backlog)
 * ikut kesimpen ke tabel attendances. Root cause paling mungkin: baris kode
 * ini belum ke-deploy / OPcache masih nahan versi lama pas backlog itu masuk,
 * BUKAN bug di urutan guard-nya sendiri (guard employeeNo prefix "S" sudah
 * benar posisinya, sebelum firstOrCreate). Supaya kejadian serupa gampang
 * dilacak ke depannya, sekarang tiap keputusan ignore/proses dicatat lewat
 * Log::debug (bukan Log::info, biar gak berisik di log level produksi biasa).
 */
class AttendanceWebhookController extends Controller
{
    // Dikonfirmasi dari tes langsung: subEventType 75 = "Authenticated via Face" (sukses)
    private const SUCCESS_SUB_EVENT_CODES = [75];

    public function store(Request $request)
    {
        $contentType = $request->header('Content-Type', '');
        $pictureFile = null;

        if (str_contains($contentType, 'multipart/form-data')) {
            $eventRaw = $request->input('AccessControllerEvent');
            $event = $this->decodeEvent($eventRaw);
            $pictureFile = $request->file('Picture') ?? $request->file('Picture_Name');
        } else {
            $event = $request->json()->all();
        }

        if (!$event) {
            Log::debug('Webhook diabaikan: payload kosong/gagal di-decode', [
                'content_type' => $contentType,
            ]);

            return response()->json(['status' => 'ignored', 'reason' => 'no event data']);
        }

        // Heartbeat = device "ngasih tau masih hidup" tiap beberapa detik, BUKAN absensi.
        // Diabaikan diam-diam (nggak di-log) biar log gak kebanjiran - heartbeat bisa
        // datang tiap detik, jadi sengaja TIDAK ditaruh Log::debug di sini juga.
        if (($event['eventType'] ?? null) === 'heartBeat') {
            return response()->json(['status' => 'ignored', 'reason' => 'heartbeat']);
        }

        if (($event['eventState'] ?? null) !== 'active') {
            Log::debug('Webhook diabaikan: eventState bukan active', [
                'eventState' => $event['eventState'] ?? null,
            ]);

            return response()->json(['status' => 'ignored', 'reason' => 'not active event']);
        }

        // Data beneran (employeeNo, majorEventType, dst) itu NESTED di dalam key ini
        $detail = $event['AccessControllerEvent'] ?? null;

        if (!$detail) {
            Log::debug('Webhook diabaikan: tidak ada detail AccessControllerEvent', [
                'eventType' => $event['eventType'] ?? null,
            ]);

            return response()->json(['status' => 'ignored', 'reason' => 'no AccessControllerEvent detail']);
        }

        $employeeNo = $detail['employeeNoString'] ?? null;
        $subEventType = $detail['subEventType'] ?? null;

        // Guard 1: harus ada employeeNo & subEventType-nya termasuk kode sukses.
        // (door open/closed, percobaan gagal, dll masuk sini dan dilewati diam-diam)
        if (!$employeeNo || !in_array($subEventType, self::SUCCESS_SUB_EVENT_CODES, true)) {
            Log::debug('Webhook diabaikan: bukan event absen sukses', [
                'employeeNo' => $employeeNo,
                'subEventType' => $subEventType,
            ]);

            return response()->json(['status' => 'ignored', 'reason' => 'not a successful attendance event']);
        }

        // Guard 2: cuma proses siswa dari sistem kita (employeeNo diawali "S"),
        // bukan data enrollment lama/test yang sudah ada di device sebelum kita
        // mulai pakai (format employeeNo beda, misal "1").
        if (!$this->isSystemEmployeeNo($employeeNo)) {
            Log::debug('Webhook diabaikan: employeeNo bukan format siswa sistem kita', [
                'employeeNo' => $employeeNo,
            ]);

            return response()->json(['status' => 'ignored', 'reason' => 'employeeNo bukan format siswa sistem kita']);
        }

        // Debounce: device kadang ngirim event berkali-kali untuk 1 kejadian yang sama
        // (misal orang berdiri agak lama di depan kamera, tiap siklus deteksi dianggap event baru).
        // Kalau orang yang sama sudah absen dalam beberapa menit terakhir, jangan disimpan lagi.
        $debounceMinutes = config('facedevice.attendance_debounce_minutes', 2);
        $recentDuplicate = Attendance::where('employee_no', $employeeNo)
            ->where('event_time', '>=', now()->subMinutes($debounceMinutes))
            ->exists();

        if ($recentDuplicate) {
            Log::debug('Webhook diabaikan: duplikat dalam jeda waktu debounce', [
                'employeeNo' => $employeeNo,
                'debounceMinutes' => $debounceMinutes,
            ]);

            return response()->json(['status' => 'ignored', 'reason' => 'duplikat dalam jeda waktu debounce']);
        }

        // Cari device mana yang ngirim ini, cocokin dari ipAddress di payload
        $senderIp = $event['ipAddress'] ?? null;
        $device = $senderIp ? Device::where('ip_address', $senderIp)->first() : null;

        if (!$device) {
            Log::warning('Webhook diterima tapi device pengirim tidak ketemu di tabel devices', [
                'sender_ip' => $senderIp,
                'employee_no' => $employeeNo,
            ]);
        }

        Log::info('Absensi realtime diterima', [
            'employee_no' => $employeeNo,
            'device' => $device->name ?? "IP tidak dikenal: {$senderIp}",
        ]);

        $picturePath = null;
        if ($pictureFile) {
            $picturePath = $pictureFile->store('attendance-photos', 'public');
        }

        $eventTime = $this->parseEventTime($event['dateTime'] ?? null);
        $attendanceStatus = $this->determineAttendanceStatus($employeeNo, $eventTime);

        if (!$attendanceStatus) {
            Log::debug('Webhook diabaikan: sudah ada hadir & pulang hari ini', [
                'employeeNo' => $employeeNo,
                'eventTime' => $eventTime->toDateTimeString(),
            ]);

            return response()->json(['status' => 'ignored', 'reason' => 'sudah absen hadir & pulang hari ini']);
        }

        $attendance = Attendance::firstOrCreate(
            [
                'employee_no' => $employeeNo,
                'event_time' => $eventTime,
                'serial_no' => $detail['serialNo'] ?? null,
            ],
            [
                'device_id' => $device?->id,
                'student_id' => StudentFaceRegistration::studentIdFromEmployeeNo($employeeNo),
                'name' => $detail['name'] ?? null,
                'attendance_status' => $attendanceStatus,
                'picture_path' => $picturePath,
                'raw_payload' => $event,
            ]
        );

        Log::info('Absensi realtime tersimpan', [
            'attendance_id' => $attendance->id,
            'employee_no' => $employeeNo,
            'device_id' => $device?->id,
        ]);

        return response()->json(['status' => 'ok']);
    }

    /**
     * Device yang sama dipakai buat scan "hadir" maupun "pulang" (bukan reader
     * terpisah), jadi statusnya ditentukan dari URUTAN scan employeeNo ini
     * pada HARI itu (zona waktu lokal, bukan UTC - soalnya "hari" buat orang
     * Indonesia ya berdasarkan WIB, bukan tanggal UTC yang beda ~7 jam):
     *   - belum ada catatan "hadir" hari ini -> ini scan "hadir"
     *   - sudah ada "hadir" tapi belum ada "pulang" -> ini scan "pulang"
     *   - sudah ada keduanya -> return null (biar dilewati, jangan disimpan
     *     dobel; debounce 2 menit sudah nangkep kasus spam-scan berdekatan,
     *     ini buat kasus scan ke-3 dst di hari yang sama)
     */
    private function determineAttendanceStatus(string $employeeNo, Carbon $eventTime): ?string
    {
        $localTimezone = config('facedevice.local_timezone', 'Asia/Jakarta');
        $localDay = $eventTime->copy()->setTimezone($localTimezone);

        $dayStartUtc = $localDay->copy()->startOfDay()->utc();
        $dayEndUtc = $localDay->copy()->endOfDay()->utc();

        $todayStatuses = Attendance::where('employee_no', $employeeNo)
            ->whereBetween('event_time', [$dayStartUtc, $dayEndUtc])
            ->pluck('attendance_status');

        if (!$todayStatuses->contains('hadir')) {
            return 'hadir';
        }

        if (!$todayStatuses->contains('pulang')) {
            return 'pulang';
        }

        return null;
    }

    /**
     * Decode field "AccessControllerEvent" dari multipart form. Ini dibungkus
     * try/catch + cek json_last_error soalnya kalau device ngirim JSON yang
     * malformed, json_decode bisa balikin null diam-diam dan bikin susah
     * dibedain dari "field-nya emang kosong".
     */
    private function decodeEvent(?string $eventRaw): ?array
    {
        if (!$eventRaw) {
            return null;
        }

        $decoded = json_decode($eventRaw, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            Log::warning('Gagal decode JSON AccessControllerEvent', [
                'json_error' => json_last_error_msg(),
                'raw_snippet' => substr($eventRaw, 0, 200),
            ]);

            return null;
        }

        return $decoded;
    }

    /**
     * Device ngirim dateTime dengan offset eksplisit, misal
     * "2026-07-20T14:36:36+07:00" (WIB). App timezone kita UTC
     * (config/app.php), jadi kita HARUS parse dengan offset itu lalu
     * konversi eksplisit ke UTC sebelum disimpan - kalau string-nya
     * dioper mentah ke kolom datetime, MySQL/Eloquent cuma nyimpen angka
     * jamnya apa adanya ("14:36:36") tanpa tau itu WIB, jadi ke-artikan
     * seolah itu udah UTC (selisih 7 jam, bikin debounce & laporan salah).
     */
    private function parseEventTime(?string $dateTime): Carbon
    {
        if (!$dateTime) {
            return now();
        }

        try {
            return Carbon::parse($dateTime)->utc();
        } catch (\Exception $e) {
            Log::warning('Gagal parse dateTime dari device, pakai waktu server sebagai fallback', [
                'raw_dateTime' => $dateTime,
                'error' => $e->getMessage(),
            ]);

            return now();
        }
    }

    /**
     * Cek apakah employeeNo ini format siswa dari sistem kita ("S..."),
     * bukan enrollment lama/test di device (misal "1", "2", dst).
     * Prefix bisa di-override lewat config kalau suatu saat berubah.
     */
    private function isSystemEmployeeNo(string $employeeNo): bool
    {
        $prefix = config('facedevice.employee_no_prefix', 'S');

        return str_starts_with($employeeNo, $prefix);
    }
}