<?php
/**
 * Biometric attendance — shared helpers.
 *
 * Supports two capture paths:
 *   1. Physical terminals (ZKTeco fingerprint / face) that push punches via the
 *      ADMS/iclock protocol to attendance/device_endpoint.php — works over both
 *      wired Ethernet and Wi-Fi.
 *   2. A browser kiosk (wired USB webcam or wireless IP camera) that captures a
 *      MediaPipe face descriptor and matches it server-side (same descriptor
 *      format as the canteen module).
 *
 * A matched student punch marks that student present for the day in the core
 * `attendance` table.
 */

if (!function_exists('biometric_ensure_tables')) {

    function biometric_ensure_tables(mysqli $conn): void
    {
        static $done = false;
        if ($done) return;
        $done = true;
        $conn->query("CREATE TABLE IF NOT EXISTS biometric_devices (
            id INT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(120) NOT NULL,
            serial_number VARCHAR(80) NOT NULL,
            device_type ENUM('fingerprint','face','hybrid') NOT NULL DEFAULT 'fingerprint',
            connection ENUM('wired','wireless') NOT NULL DEFAULT 'wired',
            ip_address VARCHAR(45) NULL,
            location VARCHAR(120) NULL,
            direction ENUM('in','out','auto') NOT NULL DEFAULT 'auto',
            active TINYINT(1) NOT NULL DEFAULT 1,
            last_seen DATETIME NULL,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            UNIQUE KEY uniq_serial (serial_number)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");

        $conn->query("CREATE TABLE IF NOT EXISTS biometric_enrollments (
            id INT AUTO_INCREMENT PRIMARY KEY,
            person_type ENUM('student','staff') NOT NULL DEFAULT 'student',
            person_id INT NOT NULL,
            person_name VARCHAR(120) NULL,
            method ENUM('fingerprint','face') NOT NULL,
            device_user_id VARCHAR(40) NULL,
            face_descriptor MEDIUMTEXT NULL,
            device_id INT NULL,
            enrolled_by INT NULL,
            enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            KEY idx_person (person_type, person_id),
            KEY idx_deviceuser (device_user_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");

        $conn->query("CREATE TABLE IF NOT EXISTS biometric_punches (
            id INT AUTO_INCREMENT PRIMARY KEY,
            device_id INT NULL,
            raw_user_id VARCHAR(40) NULL,
            person_type ENUM('student','staff') NULL,
            person_id INT NULL,
            method ENUM('fingerprint','face') NOT NULL DEFAULT 'fingerprint',
            direction ENUM('in','out') NOT NULL DEFAULT 'in',
            punch_time DATETIME NOT NULL,
            matched TINYINT(1) NOT NULL DEFAULT 0,
            attendance_written TINYINT(1) NOT NULL DEFAULT 0,
            source VARCHAR(20) NOT NULL DEFAULT 'device',
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            KEY idx_time (punch_time), KEY idx_matched (matched)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
    }

    function biometric_can_manage(string $role): bool
    {
        return in_array($role, ['admin', 'headteacher', 'supervisor', 'director'], true);
    }

    /** Look up a device by serial (SN), returns row or null. */
    function biometric_device_by_serial(mysqli $conn, string $sn): ?array
    {
        $stmt = $conn->prepare("SELECT * FROM biometric_devices WHERE serial_number=? AND active=1");
        $stmt->bind_param('s', $sn); $stmt->execute();
        $row = $stmt->get_result()->fetch_assoc(); $stmt->close();
        return $row ?: null;
    }

    /** Resolve a device user id to an enrolled person. */
    function biometric_resolve_user(mysqli $conn, string $deviceUserId): ?array
    {
        $stmt = $conn->prepare("SELECT person_type, person_id, person_name FROM biometric_enrollments
                                WHERE device_user_id=? AND method='fingerprint' LIMIT 1");
        $stmt->bind_param('s', $deviceUserId); $stmt->execute();
        $row = $stmt->get_result()->fetch_assoc(); $stmt->close();
        return $row ?: null;
    }

    /**
     * Record a punch and, when it maps to a student, mark them present for the day.
     * $person may be null (unmatched punch kept for manual assignment).
     */
    function biometric_record_punch(mysqli $conn, ?int $deviceId, string $rawUserId, ?array $person,
                                    string $method, string $direction, string $punchTime, string $source = 'device'): int
    {
        biometric_ensure_tables($conn);
        $pt = $person['person_type'] ?? null;
        $pid = isset($person['person_id']) ? (int) $person['person_id'] : null;
        $matched = $person ? 1 : 0;
        $written = 0;

        if ($matched && $pt === 'student' && $pid) {
            $written = biometric_write_attendance($conn, $pid, substr($punchTime, 0, 10)) ? 1 : 0;
        }

        $stmt = $conn->prepare("INSERT INTO biometric_punches
            (device_id, raw_user_id, person_type, person_id, method, direction, punch_time, matched, attendance_written, source)
            VALUES (?,?,?,?,?,?,?,?,?,?)");
        $stmt->bind_param('ississsiis', $deviceId, $rawUserId, $pt, $pid, $method, $direction, $punchTime, $matched, $written, $source);
        $stmt->execute(); $id = $stmt->insert_id; $stmt->close();
        return $id;
    }

    /** Upsert a present record for a student on a date. Returns true if written. */
    function biometric_write_attendance(mysqli $conn, int $studentId, string $date): bool
    {
        // Resolve the student's class for the attendance row.
        $s = $conn->prepare("SELECT class FROM students WHERE id=?");
        $s->bind_param('i', $studentId); $s->execute();
        $stu = $s->get_result()->fetch_assoc(); $s->close();
        if (!$stu) return false;
        $class = $stu['class'];

        $chk = $conn->prepare("SELECT id FROM attendance WHERE student_id=? AND attendance_date=? LIMIT 1");
        $chk->bind_param('is', $studentId, $date); $chk->execute();
        $exists = $chk->get_result()->fetch_assoc(); $chk->close();
        if ($exists) {
            $conn->query("UPDATE attendance SET status='present' WHERE id=" . (int) $exists['id']);
            return true;
        }
        $note = 'Biometric clock-in';
        $ins = $conn->prepare("INSERT INTO attendance (student_id, class_name, attendance_date, status, recorded_by, notes)
                               VALUES (?,?,?,'present',NULL,?)");
        $ins->bind_param('isss', $studentId, $class, $date, $note);
        $ok = $ins->execute(); $ins->close();
        return $ok;
    }

    /**
     * Compare two MediaPipe descriptors (arrays of {x,y,z}). Returns the mean
     * per-landmark Euclidean distance (lower = more similar), or null if invalid.
     */
    function biometric_descriptor_distance(array $a, array $b): ?float
    {
        $n = min(count($a), count($b));
        if ($n < 100) return null;
        $sum = 0.0;
        for ($i = 0; $i < $n; $i++) {
            $dx = ($a[$i]['x'] ?? 0) - ($b[$i]['x'] ?? 0);
            $dy = ($a[$i]['y'] ?? 0) - ($b[$i]['y'] ?? 0);
            $dz = ($a[$i]['z'] ?? 0) - ($b[$i]['z'] ?? 0);
            $sum += sqrt($dx * $dx + $dy * $dy + $dz * $dz);
        }
        return $sum / $n;
    }

    /**
     * Match a captured face descriptor against enrolled students.
     * Returns ['person_type','person_id','person_name','distance'] or null.
     */
    function biometric_match_face(mysqli $conn, array $descriptor, float $threshold = 0.06): ?array
    {
        biometric_ensure_tables($conn);
        $res = $conn->query("SELECT person_type, person_id, person_name, face_descriptor
                             FROM biometric_enrollments WHERE method='face' AND face_descriptor IS NOT NULL");
        $best = null; $bestDist = PHP_FLOAT_MAX;
        while ($res && $row = $res->fetch_assoc()) {
            $enr = json_decode($row['face_descriptor'], true);
            if (!is_array($enr)) continue;
            $d = biometric_descriptor_distance($descriptor, $enr);
            if ($d !== null && $d < $bestDist) {
                $bestDist = $d;
                $best = ['person_type' => $row['person_type'], 'person_id' => (int) $row['person_id'], 'person_name' => $row['person_name']];
            }
        }
        if ($best && $bestDist <= $threshold) {
            $best['distance'] = round($bestDist, 5);
            return $best;
        }
        return null;
    }

    /** Recommended, tested hardware (shown in the UI). */
    function biometric_recommended_devices(): array
    {
        return [
            ['ZKTeco SpeedFace-V5L', 'Face + fingerprint', 'Wi-Fi + Ethernet', 'Best all-in-one; on-device face & fingerprint, ADMS push.'],
            ['ZKTeco F18 / F22',     'Fingerprint',        'Ethernet + Wi-Fi', 'Reliable fingerprint terminal for doorways.'],
            ['ZKTeco MB460',         'Face + fingerprint', 'Ethernet + Wi-Fi', 'Large-capacity hybrid terminal.'],
            ['USB UVC Webcam',       'Face (browser kiosk)', 'Wired USB',      'Any standard USB webcam for the browser kiosk.'],
            ['ONVIF / RTSP IP Camera', 'Face (browser kiosk)', 'Wireless',     'Wi-Fi IP camera streamed into the kiosk page.'],
        ];
    }
}
