<?php
// Minimal ZKTeco-compatible library (UDP 4370) to fetch attendance
// Note: This is a lightweight implementation sufficient for connect/disconnect and get attendance logs
// Tested with common ZKTeco-compatible firmware. Ronanjack devices generally use the same protocol.
// This is not a full implementation; for production, consider a mature library.

if (!defined('LUNA_APP')) {
    http_response_code(403);
    exit('Forbidden');
}

class ZKLibrary
{
    private string $ip;
    private int $port;
    private $socket = null;
    private int $sessionId = 0;
    private int $replyId = 0;

    // Commands
    private const CMD_CONNECT = 0x03e8;
    private const CMD_EXIT = 0x03e9;
    private const CMD_ACK_OK = 0x07d0;
    private const CMD_REG_EVENT = 0x01f5;
    private const CMD_DISABLEDEVICE = 0x0258;
    private const CMD_ENABLEDEVICE = 0x0259;
    private const CMD_ATTLOG_RRQ = 0x0120;

    public function __construct(string $ip, int $port = 4370)
    {
        $this->ip = $ip;
        $this->port = $port;
    }

    public function connect(int $timeoutSec = 3): bool
    {
        $this->socket = @fsockopen("udp://{$this->ip}", $this->port, $errno, $errstr, $timeoutSec);
        if (!$this->socket) {
            return false;
        }
        stream_set_timeout($this->socket, $timeoutSec);
        return $this->sendCommand(self::CMD_CONNECT);
    }

    public function disconnect(): void
    {
        if ($this->socket) {
            // try polite exit
            $this->sendCommand(self::CMD_EXIT);
            fclose($this->socket);
            $this->socket = null;
        }
    }

    public function disableDevice(): bool
    {
        return $this->sendCommand(self::CMD_DISABLEDEVICE);
    }

    public function enableDevice(): bool
    {
        return $this->sendCommand(self::CMD_ENABLEDEVICE);
    }

    public function getAttendance(): array
    {
        // Request all attendance logs
        if (!$this->sendCommand(self::CMD_ATTLOG_RRQ)) {
            return [];
        }
        $data = $this->receivePacket(8192);
        if ($data === null) {
            return [];
        }
        // The payload follows a common CSV-like format: PIN\tVERIFYMODE\tDATE TIME\tWORKCODE\n
        // We'll attempt to split lines robustly.
        $payload = $data['payload'] ?? '';
        $payload = is_string($payload) ? $payload : '';
        if ($payload === '') return [];

        $lines = preg_split("/\r?\n/", trim($payload));
        $logs = [];
        foreach ($lines as $line) {
            if ($line === '') continue;
            $parts = preg_split("/\s+|\t/", trim($line));
            if (count($parts) < 3) continue;
            // Try to recover fields: last two parts are date and time (or combined). Join to one timestamp.
            $pin = $parts[0];
            $datetime = $parts[count($parts)-2] . ' ' . $parts[count($parts)-1];
            $verify = $parts[1] ?? '';
            $logs[] = [
                'pin' => $pin,
                'verify' => $verify,
                'datetime' => $datetime,
            ];
        }
        return $logs;
    }

    private function sendCommand(int $command, string $data = ''): bool
    {
        if (!$this->socket) return false;
        $this->replyId = ($this->replyId + 1) % 65536;
        $header = pack('vvvvv', $command, 0, 0, $this->sessionId, $this->replyId);
        $packet = $header . $data;
        // checksum (little endian) over the whole packet with checksum field as 0
        $checksum = $this->calculateChecksum($packet);
        $header = pack('vvvvv', $command, 0, $checksum, $this->sessionId, $this->replyId);
        $packet = $header . $data;
        $bytes = fwrite($this->socket, $packet);
        if ($bytes === false) return false;
        $resp = $this->receivePacket();
        if ($resp === null) return false;
        // Save session id from first response
        if ($this->sessionId === 0 && isset($resp['session'])) {
            $this->sessionId = $resp['session'];
        }
        // Consider ACK OK as success
        return in_array($resp['command'], [self::CMD_ACK_OK, $command], true);
    }

    private function receivePacket(int $max = 4096): ?array
    {
        if (!$this->socket) return null;
        $raw = fread($this->socket, $max);
        if ($raw === false || strlen($raw) < 8) {
            return null;
        }
        // Unpack header
        $un = unpack('vcommand/vlength/vchecksum/vsession/vreply', substr($raw, 0, 10));
        $payload = substr($raw, 10);
        return [
            'command' => (int)$un['command'],
            'length' => (int)$un['length'],
            'checksum' => (int)$un['checksum'],
            'session' => (int)$un['session'],
            'reply' => (int)$un['reply'],
            'payload' => $payload,
        ];
    }

    private function calculateChecksum(string $buffer): int
    {
        $l = strlen($buffer);
        $checksum = 0;
        for ($i = 0; $i < $l; $i++) {
            $checksum += ord($buffer[$i]);
            $checksum &= 0xFFFF; // keep 16-bit
        }
        $checksum = ($checksum & 0xFFFF);
        return $checksum;
    }
}


