```php
<?php
/*
 * Upgraded ZKLib PHP for ZKTeco/ESSL biometric devices
 * Supports: connect, disconnect, getAttendance, getUser
 * Device: Tested with X2008 (ZLM60_TFT), K14, iFace, etc.
 * Updated: Fixed UDP to TCP, added detailed error handling for connection issues
 */

class ZKLib {
    var $ip;
    var $port;
    var $socket;
    var $session_id;
    var $reply_id;

    function __construct($ip, $port = 4370) {
        $this->ip = $ip;
        $this->port = $port;
        $this->session_id = 0;
        $this->reply_id = 0;
    }

    function connect() {
        // Validate IP and port
        if (!filter_var($this->ip, FILTER_VALIDATE_IP)) {
            error_log("ZKLib connect failed: Invalid IP address {$this->ip}");
            return "Invalid IP address: {$this->ip}";
        }
        if (!is_numeric($this->port) || $this->port < 1 || $this->port > 65535) {
            error_log("ZKLib connect failed: Invalid port {$this->port}");
            return "Invalid port: {$this->port}";
        }

        // Attempt TCP connection
        $this->socket = @fsockopen($this->ip, $this->port, $errno, $errstr, 5);
        if (!$this->socket) {
            $error = "ZKLib connect failed: fsockopen error [{$errno}] {$errstr}";
            error_log($error);
            return $error;
        }

        // Set stream options
        stream_set_timeout($this->socket, 2);
        stream_set_blocking($this->socket, true);

        // Send connect command
        $ucommand = 1000; // CMD_CONNECT
        $command_string = '';
        $buf = $this->createHeader($ucommand, $command_string);
        $write_result = fwrite($this->socket, $buf);
        if ($write_result === false) {
            $error = "ZKLib connect failed: fwrite failed during connect";
            error_log($error);
            fclose($this->socket);
            $this->socket = null;
            return $error;
        }

        // Read response
        $resp = fread($this->socket, 1024);
        if ($resp === false || strlen($resp) < 8) {
            $error = "ZKLib connect failed: Invalid or no response from device (length: " . (strlen($resp) ?: '0') . ")";
            error_log($error);
            fclose($this->socket);
            $this->socket = null;
            return $error;
        }

        // Parse session ID
        $this->session_id = unpack("S", substr($resp, 4, 2))[1];
        if ($this->session_id == 0) {
            $error = "ZKLib connect failed: Invalid session ID received";
            error_log($error);
            fclose($this->socket);
            $this->socket = null;
            return $error;
        }

        error_log("ZKLib connected successfully to {$this->ip}:{$this->port}, session_id={$this->session_id}");
        return true;
    }

    function disconnect() {
        if ($this->socket) {
            $ucommand = 1001; // CMD_EXIT
            $buf = $this->createHeader($ucommand, '', $this->session_id);
            fwrite($this->socket, $buf);
            fread($this->socket, 1024); // Read response
            fclose($this->socket);
            $this->socket = null;
            error_log("ZKLib disconnected from {$this->ip}:{$this->port}");
        }
        return true;
    }

    private function createHeader($command, $command_string = '', $session_id = 0, $reply_id = null) {
        if ($reply_id === null) $reply_id = ++$this->reply_id;
        $chksum = 0;
        $length = 8 + strlen($command_string);
        $buf = pack("SSSS", $command, $chksum, $session_id, $reply_id) . $command_string;
        $chksum = $this->zkChecksum($buf);
        $buf = pack("SSSS", $command, $chksum, $session_id, $reply_id) . $command_string;
        return $buf;
    }

    private function zkChecksum($buffer) {
        $l = strlen($buffer);
        $chksum = 0;
        for ($i = 0; $i < $l; $i++) {
            $chksum += ord($buffer[$i]);
            if ($chksum > 65535) $chksum -= 65536;
        }
        return $chksum;
    }

    // 🟢 Fetch Users
    function getUser() {
        if (!$this->socket) {
            error_log("ZKLib getUser failed: Not connected");
            return [];
        }
        $ucommand = 9; // CMD_USERTEMP_RRQ
        $buf = $this->createHeader($ucommand, '', $this->session_id);
        $write_result = fwrite($this->socket, $buf);
        if ($write_result === false) {
            error_log("ZKLib getUser fwrite failed");
            return [];
        }
        $data = fread($this->socket, 4096);

        $users = [];
        if (strlen($data) > 10) {
            $data = substr($data, 8);
            $blocks = str_split($data, 72);
            foreach ($blocks as $block) {
                if (strlen($block) < 72) continue;
                $uid = unpack("I", substr($block, 0, 4))[1];
                $userid = trim(substr($block, 4, 24), "\0");
                $name = trim(substr($block, 28, 24), "\0");
                $role = ord($block[51]);
                $users[$uid] = [$userid, $name, $role];
            }
        }
        return $users;
    }

    // 🟢 Fetch Attendance Logs
    function getAttendance() {
        if (!$this->socket) {
            error_log("ZKLib getAttendance failed: Not connected");
            return [];
        }
        $ucommand = 13; // CMD_ATTLOG_RRQ
        $buf = $this->createHeader($ucommand, '', $this->session_id);
        $write_result = fwrite($this->socket, $buf);
        if ($write_result === false) {
            error_log("ZKLib getAttendance fwrite failed");
            return [];
        }
        $data = fread($this->socket, 8192);

        $attlogs = [];
        if (strlen($data) > 10) {
            $data = substr($data, 8);
            $records = str_split($data, 40);
            foreach ($records as $rec) {
                if (strlen($rec) < 40) continue;
                $uid = unpack("I", substr($rec, 0, 4))[1];
                $userid = trim(substr($rec, 4, 24), "\0");
                $timehex = unpack("I", substr($rec, 28, 4))[1];
                $status = ord($rec[32]);

                $timestamp = $this->decodeTime($timehex);

                $attlogs[] = [
                    "uid" => $uid,
                    "userid" => $userid,
                    "time" => $timestamp,
                    "status" => $status
                ];
            }
        }
        return $attlogs;
    }

    // Convert ZK timestamp
    private function decodeTime($t) {
        $sec = $t % 60;
        $t = intval($t / 60);
        $min = $t % 60;
        $t = intval($t / 60);
        $hour = $t % 24;
        $t = intval($t / 24);
        $day = $t % 31 + 1;
        $t = intval($t / 31);
        $month = $t % 12 + 1;
        $t = intval($t / 12);
        $year = $t + 2000;
        return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year, $month, $day, $hour, $min, $sec);
    }
}
?>
