<?php
/**
 * Binance WebSocket Chat Client
 * Handles WebSocket connections to Binance chat system
 * Uses Ratchet/Pawl for WebSocket client
 */

require_once __DIR__ . '/../vendor/autoload.php';

use Ratchet\Client\WebSocket;
use Ratchet\Client\Connector;
use React\EventLoop\Factory;

class BinanceWebSocketClient {
    private $wssUrl;
    private $listenKey;
    private $listenToken;
    private $loop;
    private $connector;
    private $connection;
    private $connected = false;
    private $ready = false;
    private $messageQueue = [];
    private $lastError = null;
    
    public function __construct($wssUrl, $listenKey, $listenToken) {
        $this->wssUrl = $wssUrl;
        $this->listenKey = $listenKey;
        // Use token as-is (Binance docs show token should include TOKEN prefix)
        $this->listenToken = $listenToken;
        $this->loop = Factory::create();
        $this->connector = new Connector($this->loop);
    }
    
    /**
     * Connect to WebSocket
     */
    public function connect() {
        // According to Binance documentation, the token should be used as-is (not URL encoded)
        // The token already includes "TOKEN" prefix from Binance API
        // URL format: wss://im.binance.com:443/chat/<listenKey>?token=<listenToken>&clientType=web
        
        // Build WebSocket URL - use token as-is (no URL encoding)
        // According to Binance Python example, token is used directly in string concatenation
        // Python's string concatenation handles URL encoding automatically, but in PHP we need to be careful
        // Try using the token as-is first (as shown in Binance docs)
        $wsUrl = $this->wssUrl . "/" . $this->listenKey . "?token=" . $this->listenToken . "&clientType=web";
        
        error_log("Connecting to WebSocket: " . str_replace($this->listenToken, 'TOKEN***', $wsUrl));
        error_log("Token length: " . strlen($this->listenToken) . " chars");
        error_log("ListenKey length: " . strlen($this->listenKey) . " chars");
        
        // Connector is callable - invoke it
        $promise = call_user_func($this->connector, $wsUrl);
        
        $promise->then(function(WebSocket $conn) {
            $this->connection = $conn;
            $this->connected = true;
            error_log("WebSocket connected successfully");
            
            // Track if we received an error
            $errorReceived = false;
            
            // Handle incoming messages
            $conn->on('message', function($msg) use (&$errorReceived) {
                $data = json_decode($msg, true);
                if ($data && isset($data['type']) && $data['type'] === 'error') {
                    $errorReceived = true;
                    $errorContent = $data['content'] ?? 'Unknown error';
                    
                    // Store error immediately for checking
                    $this->lastError = $errorContent;
                    
                    error_log("ERROR: Received error message immediately after connection: " . json_encode($data));
                    
                    // If it's ILLEGAL_PARAM, log specific guidance and close connection
                    if (strpos($errorContent, 'ILLEGAL_PARAM') !== false) {
                        error_log("ILLEGAL_PARAM error - This usually means:");
                        error_log("  1. Token/listenKey format is incorrect");
                        error_log("  2. Credentials may be expired (get fresh credentials)");
                        error_log("  3. URL encoding issue with token/listenKey");
                        error_log("  4. Multiple simultaneous connections with same credentials not allowed");
                        // Close connection immediately
                        $this->connection->close();
                    }
                    
                    // Don't mark as ready if we got an error
                    $this->connected = false;
                    $this->ready = false;
                }
                $this->handleMessage($msg);
            });
            
            // Handle connection close
            $conn->on('close', function($code = null, $reason = null) {
                error_log("WebSocket closed. Code: $code, Reason: $reason");
                $this->connected = false;
                $this->ready = false;
            });
            
            // Handle errors
            $conn->on('error', function($error) {
                error_log("WebSocket error: " . $error->getMessage());
                $this->connected = false;
                $this->ready = false;
            });
            
            // Wait a moment for connection to stabilize (reduced from 2 to 1 second to reduce CPU usage)
            // Binance might send connection confirmation or error immediately
            $this->loop->addTimer(1, function() use (&$errorReceived) {
                // Only mark as ready if we haven't received an error and still connected
                if ($this->connected && !$errorReceived) {
                    $this->ready = true;
                    error_log("WebSocket ready to send messages");
                    // Send any queued messages
                    $this->processMessageQueue();
                } else {
                    if ($errorReceived) {
                        error_log("WebSocket received error, not marking as ready");
                    } else {
                        error_log("WebSocket connection lost before ready");
                    }
                }
            });
            
        }, function(\Exception $e) {
            error_log("WebSocket connection failed: " . $e->getMessage());
            $this->connected = false;
            $this->ready = false;
            throw $e;
        });
        
        // Run event loop with a timeout to establish connection
        // Use very short timers to reduce CPU usage and prevent resource overuse
        $connectionEstablished = false;
        $errorDetected = false;
        
        $this->loop->addTimer(1, function() use (&$connectionEstablished, &$errorDetected) {
            // Check if we got an ILLEGAL_PARAM error
            if ($this->lastError && strpos($this->lastError, 'ILLEGAL_PARAM') !== false) {
                $errorDetected = true;
                error_log("ILLEGAL_PARAM detected in timer - stopping loop immediately");
                $this->loop->stop();
                return;
            }
            
            if ($this->connected && $this->ready) {
                $connectionEstablished = true;
                error_log("Connection established, stopping initial loop run");
                $this->loop->stop();
            }
        });
        
        // Maximum timeout to prevent infinite blocking (reduced to 2 seconds to prevent CPU overuse)
        $this->loop->addTimer(2, function() use (&$errorDetected) {
            // Check for ILLEGAL_PARAM before logging timeout
            if ($this->lastError && strpos($this->lastError, 'ILLEGAL_PARAM') !== false) {
                $errorDetected = true;
                error_log("ILLEGAL_PARAM detected - stopping loop (not a timeout)");
            } else {
                error_log("Connection timeout reached, stopping loop");
            }
            $this->loop->stop();
        });
        
        // Run event loop to establish connection (will stop after timer)
        // Very short timeout to reduce CPU usage
        $this->loop->run();
        
        // If ILLEGAL_PARAM was detected, throw exception to prevent further attempts
        // This MUST happen immediately after loop stops to prevent any retries
        if ($errorDetected || ($this->lastError && strpos($this->lastError, 'ILLEGAL_PARAM') !== false)) {
            $errorMsg = "ILLEGAL_PARAM: Binance rejected the connection. Credentials may be invalid or expired.";
            error_log("CRITICAL: " . $errorMsg);
            error_log("Stopping all connection attempts immediately to prevent CPU overuse.");
            // Close connection if it exists
            if ($this->connection) {
                try {
                    $this->connection->close();
                } catch (\Exception $e) {
                    // Ignore close errors
                }
            }
            // Reset connection state
            $this->connected = false;
            $this->ready = false;
            // Throw exception to stop script execution
            throw new \Exception($errorMsg);
        }
    }
    
    /**
     * Send message via WebSocket
     * Format according to Binance documentation:
     * {
     *   "type":"text",
     *   "uuid":"1682102167020",
     *   "orderNo":"20476365073825144832",
     *   "content":"xxxxxx",
     *   "self":true,
     *   "clientType":"web",
     *   "createTime":{{timestamp}},
     *   "sendStatus":0
     * }
     */
    public function sendMessage($orderNo, $content) {
        // Ensure orderNo is a string
        $orderNoStr = (string)$orderNo;
        
        // Generate UUID (timestamp in milliseconds + random)
        $uuid = (string)(time() * 1000) . rand(1000, 9999);
        $createTime = time() * 1000;
        
        // Message format according to Binance documentation
        $messageData = [
            'type' => 'text',
            'uuid' => $uuid,
            'orderNo' => $orderNoStr,
            'content' => $content,
            'self' => true,
            'clientType' => 'web',
            'createTime' => $createTime,
            'sendStatus' => 0
        ];
        
        if (!$this->connected || !$this->connection) {
            // Queue message if not connected
            $this->messageQueue[] = ['orderNo' => $orderNoStr, 'content' => $content];
            error_log("WebSocket not connected, queuing message for order $orderNoStr");
            return false;
        }
        
        if (!$this->ready) {
            // Queue message if not ready yet
            $this->messageQueue[] = ['orderNo' => $orderNoStr, 'content' => $content];
            error_log("WebSocket not ready yet, queuing message for order $orderNoStr");
            return false;
        }
        
        try {
            // Clear any previous error before sending
            $this->lastError = null;
            
            // Send the message according to Binance format
            $jsonMessage = json_encode($messageData);
            error_log("Sending message to order $orderNoStr");
            error_log("Message data: " . json_encode($messageData, JSON_PRETTY_PRINT));
            
            // Clear any previous error before sending
            $this->lastError = null;
            
            $this->connection->send($jsonMessage);
            error_log("Message sent to WebSocket connection");
            
            // Run the event loop briefly to process any responses from Binance
            // This is critical - without running the loop, we won't receive error responses
            $responseProcessed = false;
            $this->loop->addTimer(3, function() use (&$responseProcessed) {
                $responseProcessed = true;
                $this->loop->stop();
            });
            
            // Run loop for a short time to process responses
            $this->loop->run();
            
            // Check if we got an error response
            if ($this->lastError) {
                error_log("⚠️ Binance returned error after sending message: " . $this->lastError);
                return false;
            }
            
            error_log("Message sent via WebSocket for order $orderNoStr (no error response received)");
            return true;
        } catch (\Exception $e) {
            error_log("Error sending WebSocket message: " . $e->getMessage());
            // Queue for retry
            $this->messageQueue[] = ['orderNo' => $orderNoStr, 'content' => $content];
            return false;
        }
    }
    
    /**
     * Process queued messages
     */
    private function processMessageQueue() {
        if (empty($this->messageQueue) || !$this->connected || !$this->ready) {
            return;
        }
        
        foreach ($this->messageQueue as $index => $queuedMessage) {
            try {
                $orderNo = $queuedMessage['orderNo'];
                $content = $queuedMessage['content'];
                
                // Rebuild message with Binance format
                $uuid = (string)(time() * 1000) . rand(1000, 9999);
                $createTime = time() * 1000;
                
                $messageData = [
                    'type' => 'text',
                    'uuid' => $uuid,
                    'orderNo' => $orderNo,
                    'content' => $content,
                    'self' => true,
                    'clientType' => 'web',
                    'createTime' => $createTime,
                    'sendStatus' => 0
                ];
                
                $jsonMessage = json_encode($messageData);
                $this->connection->send($jsonMessage);
                unset($this->messageQueue[$index]);
                error_log("Sent queued message for order: $orderNo");
                
                // Small delay between messages to avoid rate limiting
                usleep(500000); // 0.5 seconds
            } catch (\Exception $e) {
                error_log("Error sending queued message: " . $e->getMessage());
            }
        }
        
        // Re-index array
        $this->messageQueue = array_values($this->messageQueue);
    }
    
    /**
     * Send image via WebSocket
     * @param string $orderNo Order number
     * @param string $imageUrl The image URL from Binance (after upload)
     * @return bool Success status
     */
    public function sendImage($orderNo, $imageUrl) {
        // Ensure orderNo is a string
        $orderNoStr = (string)$orderNo;
        
        // Generate UUID (timestamp in milliseconds + random)
        $uuid = (string)(time() * 1000) . rand(1000, 9999);
        $createTime = time() * 1000;
        
        // Message format for sending images
        // According to Binance documentation (How to handle C2C-messages_v7.4.pdf):
        // After uploading image to pre-signed URL, send the "imageUrl" in p2p chat
        // The message format uses type="text" (same as regular text messages)
        // Binance will automatically detect and display the image URL as an image
        $messageData = [
            'type' => 'text',  // Use 'text' type, not 'image' (per Binance docs)
            'uuid' => $uuid,
            'orderNo' => $orderNoStr,
            'content' => $imageUrl,  // Image URL from Binance CDN (bin.bnbstatic.com)
            'self' => true,
            'clientType' => 'web',
            'createTime' => $createTime,
            'sendStatus' => 0
        ];
        
        error_log("Sending image URL as text message (per Binance docs: send imageUrl in p2p chat)");
        
        if (!$this->connected || !$this->connection) {
            error_log("WebSocket not connected, cannot send image for order $orderNoStr");
            return false;
        }
        
        if (!$this->ready) {
            error_log("WebSocket not ready yet, cannot send image for order $orderNoStr");
            return false;
        }
        
        try {
            // Clear any previous error before sending
            $this->lastError = null;
            
            // Send the image message
            $jsonMessage = json_encode($messageData);
            error_log("Sending image message to order $orderNoStr");
            error_log("Full message: " . $jsonMessage);
            $this->connection->send($jsonMessage);
            error_log("Image message sent to WebSocket connection");
            
            // CRITICAL: Run the event loop briefly to process responses from Binance
            $responseProcessed = false;
            $this->loop->addTimer(3, function() use (&$responseProcessed) {
                $responseProcessed = true;
                $this->loop->stop();
            });
            
            // Run loop for a short time to process responses
            $this->loop->run();
            
            // Check if we got an error response
            if ($this->lastError) {
                error_log("⚠️ Binance returned error after sending image: " . $this->lastError);
                return false;
            }
            
            error_log("Image message sent via WebSocket for order $orderNoStr (no error response received)");
            error_log("Message sent: " . substr($jsonMessage, 0, 200) . "...");
            return true;
        } catch (\Exception $e) {
            error_log("Error sending WebSocket image message: " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Handle incoming messages
     */
    private function handleMessage($msg) {
        $data = json_decode($msg, true);
        if ($data) {
            error_log("=== WebSocket Message Received ===");
            error_log("Full message: " . json_encode($data, JSON_PRETTY_PRINT));
            
            // Check for error messages
            if (isset($data['type']) && $data['type'] === 'error') {
                $errorMsg = $data['content'] ?? $data['msg'] ?? $data['message'] ?? 'Unknown error';
                error_log("⚠️ WebSocket ERROR received: " . $errorMsg);
                error_log("Full error response: " . json_encode($data, JSON_PRETTY_PRINT));
                // Store error for later retrieval
                $this->lastError = $errorMsg;
                // Don't mark as not ready on error, might be a message-specific error
            } else {
                // Log successful responses too (might be message acknowledgments)
                error_log("✓ WebSocket message received (type: " . ($data['type'] ?? 'unknown') . ")");
                if (isset($data['type']) && $data['type'] === 'image') {
                    error_log("📸 Image message response received from Binance");
                }
            }
            
            // If we receive a successful message acknowledgment, we know connection is working
            if (isset($data['type']) && $data['type'] !== 'error') {
                // Connection seems healthy
            }
        } else {
            error_log("⚠️ Received non-JSON WebSocket message: " . substr($msg, 0, 200));
        }
    }
    
    /**
     * Close connection
     */
    public function close() {
        if ($this->connection) {
            $this->connection->close();
            $this->connected = false;
            $this->ready = false;
        }
        if ($this->loop) {
            // Stop all timers and stop the loop
            $this->loop->stop();
        }
    }
    
    /**
     * Check if connected
     */
    public function isConnected() {
        return $this->connected;
    }
    
    /**
     * Get last error message
     */
    public function getLastError() {
        return $this->lastError;
    }
}

