import asyncio
import json
import logging
import platform
import subprocess
import threading
import time
from typing import Dict, Optional
import socketio

# Try to import psutil for system metrics (CPU, RAM)
try:
    import psutil
    PSUTIL_AVAILABLE = True
except ImportError:
    PSUTIL_AVAILABLE = False


class GPUMetricsCollector:
    """Collect GPU metrics from various vendors."""

    @staticmethod
    def get_metrics() -> Dict:
        """Get GPU metrics from available GPU vendor."""
        # Try NVIDIA first
        metrics = GPUMetricsCollector._get_nvidia_metrics()
        if metrics["gpu_percent"] is not None:
            return metrics

        # Try AMD (Windows)
        metrics = GPUMetricsCollector._get_amd_metrics()
        if metrics["gpu_percent"] is not None:
            return metrics

        # Try Apple Silicon
        metrics = GPUMetricsCollector._get_apple_silicon_metrics()
        if metrics["gpu_percent"] is not None:
            return metrics

        # No GPU available
        return {
            "gpu_percent": None,
            "gpu_memory_percent": None,
            "gpu_name": None,
            "gpu_vendor": None,
        }

    @staticmethod
    def _get_nvidia_metrics() -> Dict:
        """Get GPU metrics from NVIDIA GPU using nvidia-ml-py."""
        try:
            import pynvml
            pynvml.nvmlInit()
            handle = pynvml.nvmlDeviceGetHandleByIndex(0)

            # GPU utilization
            util = pynvml.nvmlDeviceGetUtilizationRates(handle)
            gpu_percent = round(util.gpu, 1)

            # GPU memory
            mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
            gpu_memory_percent = round((mem_info.used / mem_info.total) * 100, 1)

            # GPU name
            gpu_name = pynvml.nvmlDeviceGetName(handle)
            if isinstance(gpu_name, bytes):
                gpu_name = gpu_name.decode('utf-8')

            pynvml.nvmlShutdown()

            return {
                "gpu_percent": gpu_percent,
                "gpu_memory_percent": gpu_memory_percent,
                "gpu_name": gpu_name,
                "gpu_vendor": "nvidia",
            }
        except Exception:
            return {
                "gpu_percent": None,
                "gpu_memory_percent": None,
                "gpu_name": None,
                "gpu_vendor": None,
            }

    @staticmethod
    def _get_amd_metrics() -> Dict:
        """Get GPU metrics from AMD GPU on Windows using WMI."""
        if platform.system() != "Windows":
            return {
                "gpu_percent": None,
                "gpu_memory_percent": None,
                "gpu_name": None,
                "gpu_vendor": None,
            }

        try:
            # Try using WMI to get AMD GPU metrics
            import wmi
            c = wmi.WMI()

            gpu_name = None
            gpu_percent = None
            gpu_memory_percent = None

            # Look for AMD GPU in Win32_VideoController
            for gpu in c.Win32_VideoController():
                name = gpu.Name
                if name and ("AMD" in name or "Radeon" in name):
                    gpu_name = name

                    # Try to get utilization via performance counters
                    # This requires running PowerShell
                    try:
                        result = subprocess.run(
                            [
                                "powershell",
                                "-Command",
                                "Get-Counter -Counter '\\GPU Engine(*engtype_3D)\\Utilization Percentage' -SampleInterval 1 -MaxSamples 1 | Select-Object -ExpandProperty CounterSamples | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum"
                            ],
                            capture_output=True,
                            text=True,
                            timeout=5
                        )
                        if result.returncode == 0:
                            util_value = result.stdout.strip()
                            if util_value:
                                gpu_percent = round(float(util_value), 1)
                    except Exception:
                        pass

                    # Try to get memory usage
                    try:
                        # AMD memory via WMI
                        adapter_ram = gpu.AdapterRAM
                        if adapter_ram:
                            # This is total memory, we need used memory
                            # Try via performance counter
                            result = subprocess.run(
                                [
                                    "powershell",
                                    "-Command",
                                    f"Get-WmiObject Win32_PerfFormattedData_GPUPerformanceCounters_GPUMemory | Where-Object {{$_.Name -like '*{gpu_name}*'}} | Select-Object -First 1"
                                ],
                                capture_output=True,
                                text=True,
                                timeout=5
                            )
                            # Parse if available
                    except Exception:
                        pass

                    if gpu_name:
                        return {
                            "gpu_percent": gpu_percent,
                            "gpu_memory_percent": gpu_memory_percent,
                            "gpu_name": gpu_name,
                            "gpu_vendor": "amd",
                        }

            # Alternative: Try ADL (AMD Display Library) via pyadl
            try:
                from pyadl import ADLManager
                devices = ADLManager.getInstance().getDevices()
                if devices:
                    device = devices[0]
                    gpu_name = device.name
                    gpu_percent = round(device.getCurrentUsage(), 1)

                    # Memory info
                    memory_info = device.getMemoryInfo()
                    if memory_info:
                        used, total = memory_info
                        gpu_memory_percent = round((used / total) * 100, 1) if total > 0 else None

                    return {
                        "gpu_percent": gpu_percent,
                        "gpu_memory_percent": gpu_memory_percent,
                        "gpu_name": gpu_name,
                        "gpu_vendor": "amd",
                    }
            except ImportError:
                pass
            except Exception:
                pass

        except ImportError:
            pass
        except Exception:
            pass

        return {
            "gpu_percent": None,
            "gpu_memory_percent": None,
            "gpu_name": None,
            "gpu_vendor": None,
        }

    @staticmethod
    def _get_apple_silicon_metrics() -> Dict:
        """Get GPU metrics from Apple Silicon (M1/M2/M3)."""
        # Only works on macOS
        if platform.system() != "Darwin":
            return {
                "gpu_percent": None,
                "gpu_memory_percent": None,
                "gpu_name": None,
                "gpu_vendor": None,
            }

        try:
            # Check if Apple Silicon
            cpu_brand = subprocess.check_output(
                ["sysctl", "-n", "machdep.cpu.brand_string"],
                stderr=subprocess.DEVNULL
            ).decode().strip()

            if "Apple" not in cpu_brand:
                return {
                    "gpu_percent": None,
                    "gpu_memory_percent": None,
                    "gpu_name": None,
                    "gpu_vendor": None,
                }

            # Get chip name
            chip_name = subprocess.check_output(
                ["sysctl", "-n", "machdep.cpu.brand_string"],
                stderr=subprocess.DEVNULL
            ).decode().strip()

            # Apple Silicon uses unified memory - report RAM usage as GPU memory
            # since GPU shares system memory
            if PSUTIL_AVAILABLE:
                mem = psutil.virtual_memory()
                # On Apple Silicon, GPU uses part of unified memory
                # We can't get exact GPU usage without sudo, so we estimate
                # based on total memory pressure
                gpu_memory_percent = round(mem.percent * 0.3, 1)  # Rough estimate

                return {
                    "gpu_percent": None,  # Can't get GPU utilization without sudo
                    "gpu_memory_percent": gpu_memory_percent,
                    "gpu_name": chip_name,
                    "gpu_vendor": "apple",
                }
        except Exception:
            pass

        return {
            "gpu_percent": None,
            "gpu_memory_percent": None,
            "gpu_name": None,
            "gpu_vendor": None,
        }


class TunnelClient:
    def __init__(
        self,
        name: str,
        server: str,
        token: str,
        ollama_port: int = 11434,
        lmstudio_port: int = 1234,
        services: Optional[list] = None,
        verbose: bool = False,
        check_interval: int = 10,  # Check services every 10 seconds
        insecure: bool = False,  # Skip SSL verification
    ):
        self.name = name
        self.server = server
        self.token = token
        self.ollama_port = ollama_port
        self.lmstudio_port = lmstudio_port
        self.services = services or ["ollama", "lmstudio"]
        self.verbose = verbose
        self.insecure = insecure
        self.namespace = "/ws/tunnel"
        self.check_interval = check_interval

        # Use synchronous client for better compatibility with Flask-SocketIO
        # Configure SSL verification
        if insecure:
            self.sio = socketio.Client(
                logger=verbose,
                engineio_logger=verbose,
                ssl_verify=False
            )
        else:
            self.sio = socketio.Client(logger=verbose, engineio_logger=verbose)
        self.running = True
        self.last_models = {}  # Store last known models
        self.services_available = {}  # Track which services are available
        self.services_status = {}  # Track detailed status (available/unavailable)
        self.last_update_time = None  # Track last update time
        self._connected = threading.Event()

        logging.basicConfig(
            level=logging.DEBUG if verbose else logging.INFO,
            format="%(asctime)s - %(levelname)s - %(message)s",
        )
        self.logger = logging.getLogger(__name__)

        self._setup_handlers()

    def _get_system_metrics(self) -> Dict:
        """Collect system metrics: CPU, RAM, GPU."""
        metrics = {
            "cpu_percent": 0,
            "ram_percent": 0,
            "ram_used_gb": 0,
            "ram_total_gb": 0,
            "gpu_percent": None,
            "gpu_memory_percent": None,
            "gpu_name": None,
            "gpu_vendor": None,
        }

        # CPU and RAM using psutil
        if PSUTIL_AVAILABLE:
            try:
                metrics["cpu_percent"] = round(psutil.cpu_percent(interval=0.1), 1)
                mem = psutil.virtual_memory()
                metrics["ram_percent"] = round(mem.percent, 1)
                metrics["ram_used_gb"] = round(mem.used / (1024**3), 1)
                metrics["ram_total_gb"] = round(mem.total / (1024**3), 1)
            except Exception as e:
                self.logger.warning(f"Failed to get CPU/RAM metrics: {e}")

        # GPU metrics (NVIDIA, Apple Silicon, etc.)
        try:
            gpu_metrics = GPUMetricsCollector.get_metrics()
            metrics.update(gpu_metrics)
        except Exception as e:
            self.logger.debug(f"GPU metrics not available: {e}")

        return metrics

    def _get_ws_url(self) -> str:
        scheme = "https" if self.server.startswith("https") else "http"
        host = self.server.replace("https://", "").replace("http://", "")
        return f"{scheme}://{host}"

    def _get_connect_url(self) -> str:
        """Get full connection URL with auth."""
        base = self._get_ws_url()
        return f"{base}?token={self.token}&tunnel_name={self.name}"

    def _setup_handlers(self):
        @self.sio.on("connect", namespace=self.namespace)
        def on_connect():
            self.logger.info("=== CONNECTED to backend! ===")
            self.logger.info(f"My session ID: {self.sio.sid}")
            self._connected.set()
            # Register services in a separate thread
            threading.Thread(target=self._register_services_sync, daemon=True).start()

        @self.sio.on("disconnect", namespace=self.namespace)
        def on_disconnect():
            self.logger.warning("=== DISCONNECTED from backend ===")
            self._connected.clear()

        @self.sio.on("registered", namespace=self.namespace)
        def handle_registered(data):
            self.logger.info(f"=== RECEIVED registered event ===")
            self.logger.info(f"Data: {data}")
            self.logger.info(f"Tunnel registered: {data.get('tunnel')}")
            self.logger.info(f"Services: {data.get('services')}")

        @self.sio.on("error", namespace=self.namespace)
        def handle_error(data):
            self.logger.error(f"=== RECEIVED error event ===")
            self.logger.error(f"Data: {data}")
            self.logger.error(f"Backend error: {data.get('message')}")

        @self.sio.on("proxy_request", namespace=self.namespace)
        def handle_proxy_request(data):
            # Filter by tunnel_name if present (for broadcast events)
            event_tunnel_name = data.get("tunnel_name")
            if event_tunnel_name and event_tunnel_name != self.name:
                self.logger.debug(f"Ignoring proxy_request for tunnel {event_tunnel_name} (we are {self.name})")
                return
                
            self.logger.info(f"\n\n>>> RECEIVED proxy_request EVENT <<<")
            self.logger.info(f"Service: {data.get('service')}")
            self.logger.info(f"Request ID: {data.get('request_id')}")
            self.logger.info(f"Stream: {data.get('stream')}")
            self.logger.info(f"Payload keys: {list(data.get('payload', {}).keys()) if data.get('payload') else 'None'}")
            self.logger.info(f"Full data: {json.dumps(data, indent=2)}")
            self.logger.info(f">>> END proxy_request EVENT <<<\n")
            # Handle in a separate thread to avoid blocking
            threading.Thread(target=self._handle_proxy_request_sync, args=(data,), daemon=True).start()

        @self.sio.on("heartbeat", namespace=self.namespace)
        def handle_heartbeat():
            self.logger.debug("Received heartbeat, sending ack")
            try:
                self.sio.emit("heartbeat_ack", namespace=self.namespace)
            except Exception as e:
                self.logger.error(f"Error sending heartbeat_ack: {e}")

        @self.sio.on("sync_request", namespace=self.namespace)
        def handle_sync_request(data):
            self.logger.info("=== RECEIVED sync_request event ===")
            self.logger.info(f"Data: {data}")
            self.logger.info("Received sync request, updating services...")
            threading.Thread(target=self._register_services_sync, daemon=True).start()

    def _register_services_sync(self):
        """Synchronous version of register services."""
        try:
            import time

            available = {}
            models_info = {}
            status_info = {}

            if "ollama" in self.services:
                is_available = self._check_service_sync("localhost", self.ollama_port)
                status_info["ollama"] = {
                    "available": is_available,
                    "port": self.ollama_port,
                }

                if is_available:
                    available["ollama"] = self.ollama_port
                    self.logger.info(f"Ollama detected on port {self.ollama_port}")

                    try:
                        ollama_models = self._fetch_ollama_models_sync()
                        models_info["ollama"] = ollama_models
                        self.logger.info(
                            f"Ollama models detected: {len(ollama_models)} models"
                        )
                    except Exception as e:
                        self.logger.warning(f"Could not fetch Ollama models: {e}")
                        models_info["ollama"] = []
                else:
                    self.logger.warning(f"Ollama not available on port {self.ollama_port}")
                    models_info["ollama"] = []

            if "lmstudio" in self.services:
                is_available = self._check_service_sync("localhost", self.lmstudio_port)
                status_info["lmstudio"] = {
                    "available": is_available,
                    "port": self.lmstudio_port,
                }

                if is_available:
                    available["lmstudio"] = self.lmstudio_port
                    self.logger.info(f"LM Studio detected on port {self.lmstudio_port}")

                    try:
                        lmstudio_models = self._fetch_lmstudio_models_sync()
                        models_info["lmstudio"] = lmstudio_models
                        self.logger.info(
                            f"LM Studio models detected: {len(lmstudio_models)} models"
                        )
                    except Exception as e:
                        self.logger.warning(f"Could not fetch LM Studio models: {e}")
                        models_info["lmstudio"] = []
                else:
                    self.logger.warning(
                        f"LM Studio not available on port {self.lmstudio_port}"
                    )
                    models_info["lmstudio"] = []

            self.logger.info(
                f"Registering services: {available} with models: {models_info}"
            )

            self.last_update_time = time.time()

            # Get system metrics
            system_metrics = self._get_system_metrics()
            self.logger.debug(f"System metrics: CPU={system_metrics['cpu_percent']}%, RAM={system_metrics['ram_percent']}%, GPU={system_metrics['gpu_percent']}%")

            try:
                self.sio.emit(
                    "register",
                    {
                        "type": "register",
                        "token": self.token,
                        "tunnel_name": self.name,
                        "services": available,
                        "models": models_info,
                        "services_status": status_info,
                        "last_update": self.last_update_time,
                        "system_metrics": system_metrics,
                    },
                    namespace=self.namespace,
                )
                self.logger.info("Successfully sent register event")
            except Exception as e:
                self.logger.error(f"Error sending register event: {e}")

            self.services_available = {k: v for k, v in available.items()}
            self.last_models = {k: v for k, v in models_info.items()}
            self.services_status = status_info

            # Start periodic check in a separate thread
            threading.Thread(target=self._check_and_update_services_sync, daemon=True).start()

        except Exception as e:
            self.logger.error(f"Error in _register_services_sync: {e}")
            self.logger.exception(e)

    def _check_service_sync(self, host: str, port: int) -> bool:
        """Check if a service is available on the given port (synchronous)."""
        import socket
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(2.0)
            result = sock.connect_ex((host, port))
            sock.close()
            return result == 0
        except Exception:
            return False

    def _fetch_ollama_models_sync(self) -> list:
        """Fetch available models from Ollama (synchronous)."""
        import urllib.request
        try:
            req = urllib.request.Request(
                f"http://localhost:{self.ollama_port}/api/tags",
                headers={"Accept": "application/json"}
            )
            with urllib.request.urlopen(req, timeout=5) as response:
                data = json.loads(response.read().decode('utf-8'))
                models = []
                if "models" in data:
                    for model in data["models"]:
                        models.append(model.get("name", "unknown"))
                return models
        except Exception as e:
            self.logger.error(f"Error fetching Ollama models: {e}")
            raise

    def _fetch_lmstudio_models_sync(self) -> list:
        """Fetch available models from LM Studio (synchronous)."""
        import urllib.request
        try:
            req = urllib.request.Request(
                f"http://localhost:{self.lmstudio_port}/v1/models",
                headers={"Accept": "application/json"}
            )
            with urllib.request.urlopen(req, timeout=5) as response:
                data = json.loads(response.read().decode('utf-8'))
                models = []
                if "data" in data:
                    for model in data["data"]:
                        models.append(
                            model.get("id", model.get("name", "unknown"))
                        )
                return models
        except Exception as e:
            self.logger.error(f"Error fetching LM Studio models: {e}")
            raise

    def _check_and_update_services_sync(self):
        """Periodically check services and update models if changed (synchronous)."""
        import time

        while self.running and self.sio.connected:
            start_time = time.time()

            self.logger.debug("Checking services status...")

            current_services = {}
            current_models = {}
            current_status = {}

            if "ollama" in self.services:
                is_available = self._check_service_sync("localhost", self.ollama_port)
                current_status["ollama"] = {
                    "available": is_available,
                    "port": self.ollama_port,
                }

                if is_available:
                    current_services["ollama"] = self.ollama_port
                    try:
                        ollama_models = self._fetch_ollama_models_sync()
                        current_models["ollama"] = ollama_models
                        self.logger.debug(f"Ollama: {len(ollama_models)} models")
                    except Exception as e:
                        self.logger.warning(f"Could not fetch Ollama models: {e}")
                        current_models["ollama"] = []
                else:
                    self.logger.warning("Ollama not available")
                    current_models["ollama"] = []

            if "lmstudio" in self.services:
                is_available = self._check_service_sync("localhost", self.lmstudio_port)
                current_status["lmstudio"] = {
                    "available": is_available,
                    "port": self.lmstudio_port,
                }

                if is_available:
                    current_services["lmstudio"] = self.lmstudio_port
                    try:
                        lmstudio_models = self._fetch_lmstudio_models_sync()
                        current_models["lmstudio"] = lmstudio_models
                        self.logger.debug(f"LM Studio: {len(lmstudio_models)} models")
                    except Exception as e:
                        self.logger.warning(f"Could not fetch LM Studio models: {e}")
                        current_models["lmstudio"] = []
                else:
                    self.logger.warning("LM Studio not available")
                    current_models["lmstudio"] = []

            services_changed = current_services != self.services_available
            models_changed = current_models != self.last_models
            status_changed = current_status != self.services_status

            if services_changed or models_changed or status_changed:
                self.logger.info("Services or models changed, updating backend...")
                self.services_available = current_services
                self.last_models = current_models
                self.services_status = current_status
                self.last_update_time = time.time()

                available_services = {
                    k: v for k, v in current_services.items() if v is not None
                }

                # Get system metrics
                system_metrics = self._get_system_metrics()

                try:
                    self.sio.emit(
                        "register",
                        {
                            "type": "register",
                            "token": self.token,
                            "tunnel_name": self.name,
                            "services": available_services,
                            "models": current_models,
                            "services_status": current_status,
                            "last_update": self.last_update_time,
                            "system_metrics": system_metrics,
                        },
                        namespace=self.namespace,
                    )
                    self.logger.info(
                        f"Updated backend with services: {list(available_services.keys())}"
                    )
                except Exception as e:
                    self.logger.error(f"Error updating backend: {e}")
            else:
                self.logger.debug("No changes detected")

            elapsed = time.time() - start_time
            sleep_time = max(0.5, self.check_interval - elapsed)

            unavailable_count = sum(
                1 for s in current_status.values() if not s.get("available", False)
            )
            if unavailable_count > 0:
                sleep_time = min(sleep_time, 5)

            time.sleep(sleep_time)

    def _handle_proxy_request_sync(self, data: dict):
        """Handle proxy request from backend (synchronous).
        
        Always uses streaming mode for optimal performance.
        """
        service = data.get("service")
        request_id = data.get("request_id")
        payload = data.get("payload")

        self.logger.info(f"=== HANDLE proxy_request ===")
        self.logger.info(f"Service: {service}")
        self.logger.info(f"Request ID: {request_id}")
        self.logger.info(f"Payload: {payload}")

        self.logger.info(
            f"Handling proxy_request: {service}/{request_id}"
        )

        if service == "ollama":
            port = self.ollama_port
        elif service == "lmstudio":
            port = self.lmstudio_port
        else:
            self.logger.error(f"Unknown service: {service}")
            return

        try:
            import socket

            # Connect to service
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(5.0)
            sock.connect(("localhost", port))

            # Build and send HTTP request
            if isinstance(payload, dict):
                method = payload.get("method", "GET")
                endpoint = payload.get("endpoint", "")
                args = payload.get("args", {})
                json_data = payload.get("json")
                
                # Add /v1 prefix for Ollama and LM Studio OpenAI-compatible API
                if service in ("ollama", "lmstudio") and not endpoint.startswith("v1/"):
                    endpoint = f"v1/{endpoint}"

                # Build HTTP request
                if args:
                    query = "&".join(f"{k}={v}" for k, v in args.items())
                    endpoint = f"{endpoint}?{query}" if endpoint else f"?{query}"

                request_line = f"{method} /{endpoint} HTTP/1.1\r\n"
                headers = "Host: localhost\r\nConnection: close\r\n"

                if json_data:
                    body = json.dumps(json_data)
                    headers += f"Content-Type: application/json\r\nContent-Length: {len(body)}\r\n\r\n"
                    request = (request_line + headers + body).encode()
                else:
                    headers += "\r\n"
                    request = (request_line + headers).encode()
            else:
                request = payload.encode() if isinstance(payload, str) else payload

            self.logger.info(f"Sending to {service}: {method} {endpoint}")
            self.logger.debug(f"Full HTTP request:\n{request.decode('utf-8', errors='replace')}")
            sock.sendall(request)

            # Always use streaming mode for better performance
            self._handle_stream_response_sync(sock, request_id, service)

            sock.close()

        except Exception as e:
            self.logger.error(f"Proxy error in _handle_proxy_request: {e}")
            self.logger.exception(e)
            # Always use stream_end for errors
            try:
                self.sio.emit(
                    "stream_end",
                    {"request_id": request_id, "error": str(e)},
                    namespace=self.namespace,
                )
            except Exception as emit_err:
                self.logger.error(f"Error emitting stream_end: {emit_err}")

    def _handle_full_response_sync(self, sock, request_id, service):
        """Handle non-streaming response - wait for complete response (synchronous)."""
        chunks = []
        sock.settimeout(30.0)
        try:
            while True:
                chunk = sock.recv(4096)
                if not chunk:
                    break
                chunks.append(chunk)
        except socket.timeout:
            self.logger.warning("Socket timeout while reading response")
        except Exception as e:
            self.logger.error(f"Error reading response: {e}")

        response = b"".join(chunks)

        # Parse HTTP response - extract body
        response_str = response.decode("utf-8", errors="replace")
        self.logger.info(f"Received response from {service}: {len(response_str)} chars")

        # Split headers and body
        try:
            header_end = response_str.find("\r\n\r\n")
            if header_end > 0:
                body = response_str[header_end + 4 :]
            else:
                body = response_str
        except Exception as e:
            self.logger.warning(f"Failed to parse response: {e}")
            body = response_str

        try:
            self.sio.emit(
                "proxy_response",
                {
                    "type": "proxy_response",
                    "request_id": request_id,
                    "status": "success",
                    "data": body,
                },
                namespace=self.namespace,
            )
            self.logger.info(f"Sent proxy_response for {request_id}")
        except Exception as e:
            self.logger.error(f"Error sending proxy_response: {e}")

    def _handle_stream_response_sync(self, sock, request_id, service):
        """Handle streaming response - forward chunks as they arrive (synchronous)."""
        self.logger.info(f"=== START _handle_stream_response ===")
        self.logger.info(f"Request ID: {request_id}, Service: {service}")

        buffer = b""
        header_ended = False
        chunk_count = 0
        body_started = False

        try:
            self.logger.info(f"Starting to read from {service}...")
            sock.settimeout(60.0)
            while True:
                self.logger.debug(f"Waiting to read chunk... (buffer size: {len(buffer)})")
                try:
                    chunk = sock.recv(4096)
                except socket.timeout:
                    self.logger.warning("Socket timeout while reading stream")
                    break
                self.logger.debug(f"Read chunk: {len(chunk)} bytes")
                if not chunk:
                    self.logger.info(f"Empty chunk received, ending stream")
                    break

                buffer += chunk

                # Parse HTTP headers first
                if not header_ended:
                    header_end = buffer.find(b"\r\n\r\n")
                    if header_end >= 0:
                        headers = buffer[:header_end].decode('utf-8', errors='replace')
                        self.logger.info(f"Received HTTP headers from {service}:\n{headers}")
                        header_ended = True
                        body_started = True
                        buffer = buffer[header_end + 4 :]
                        # Check if chunked transfer encoding is used
                        use_chunked = 'transfer-encoding' in headers.lower() and 'chunked' in headers.lower()
                        self.logger.info(f"Using chunked decoding: {use_chunked}")
                    else:
                        continue

                # For streaming: first handle HTTP chunked transfer encoding (if present), then parse SSE
                if body_started:
                    decoded_buffer = b""
                    
                    # Only strip chunked encoding if the header indicates it
                    if use_chunked:
                        # Strip HTTP chunked transfer encoding
                        # Format: <hex-size>\r\n<data>\r\n<hex-size>\r\n<data>\r\n...
                        while buffer:
                            # Check if we have chunked encoding prefix (hex number followed by \r\n)
                            newline_pos = buffer.find(b"\r\n")
                            if newline_pos < 0:
                                # No complete chunk header - append remaining data
                                decoded_buffer += buffer
                                break

                            # Try to parse chunk size
                            try:
                                size_hex = buffer[:newline_pos].decode("ascii").strip()
                                chunk_size = int(size_hex, 16)
                            except (ValueError, UnicodeDecodeError):
                                # Not chunked encoding - append remaining data
                                decoded_buffer += buffer
                                break

                            if chunk_size == 0:
                                # End of chunked stream - skip the terminator \r\n
                                buffer = buffer[newline_pos + 2:]
                                break

                            # Check if we have the full chunk data
                            data_start = newline_pos + 2
                            data_end = data_start + chunk_size
                            if len(buffer) < data_end + 2:
                                # Wait for more data - append what we have
                                decoded_buffer += buffer
                                break

                            # Extract chunk data and add to decoded buffer
                            chunk_data = buffer[data_start:data_end]
                            decoded_buffer += chunk_data
                            buffer = buffer[data_end + 2:]  # Skip trailing \r\n
                    else:
                        # No chunked encoding - use buffer as-is
                        decoded_buffer = buffer
                        buffer = b""
                    
                    # Now parse SSE format from decoded buffer
                    buffer = decoded_buffer
                    
                    # Process all complete SSE chunks
                    while True:
                        # Find next SSE chunk (data: {...}\n\n or data: {...}\r\n\r\n)
                        sse_end = buffer.find(b"\n\n")
                        if sse_end < 0:
                            sse_end = buffer.find(b"\r\n\r\n")
                        if sse_end < 0:
                            # Wait for more data
                            break

                        # Extract SSE chunk
                        sse_chunk = buffer[:sse_end]
                        buffer = buffer[sse_end + 2:]
                        if buffer.startswith(b"\r\n"):
                            buffer = buffer[2:]

                        # Parse and forward
                        if sse_chunk.strip():
                            chunk_str = sse_chunk.decode("utf-8", errors="replace")
                            # Strip 'data: ' prefix if present
                            if chunk_str.startswith("data: "):
                                chunk_str = chunk_str[6:]

                            self.logger.debug(f"Sending stream_chunk #{chunk_count+1} for {request_id}: {chunk_str[:100]}...")
                            try:
                                self.sio.emit(
                                    "stream_chunk",
                                    {"request_id": request_id, "chunk": chunk_str},
                                    namespace=self.namespace,
                                )
                                chunk_count += 1
                                self.logger.info(f"Sent chunk {chunk_count} with {len(chunk_str)} bytes")
                            except Exception as e:
                                self.logger.error(f"Error sending stream_chunk: {e}")

                    # Send any remaining data as final chunk
                    if buffer.strip():
                        chunk_str = buffer.decode("utf-8", errors="replace")
                        if chunk_str.startswith("data: "):
                            chunk_str = chunk_str[6:]
                        self.logger.debug(f"Sending final chunk for {request_id}: {chunk_str[:100]}...")
                        try:
                            self.sio.emit(
                                "stream_chunk",
                                {"request_id": request_id, "chunk": chunk_str},
                                namespace=self.namespace,
                            )
                            chunk_count += 1
                            self.logger.info(f"Sent final chunk {chunk_count} with {len(chunk_str)} bytes")
                        except Exception as e:
                            self.logger.error(f"Error sending final chunk: {e}")
                    buffer = b""

            self.logger.info(
                f"Stream ended for {request_id}, sent {chunk_count} chunks"
            )

            # Signal end of stream
            self.logger.info(f"Sending stream_end for {request_id}")
            try:
                self.sio.emit(
                    "stream_end", {"request_id": request_id}, namespace=self.namespace
                )
            except Exception as e:
                self.logger.error(f"Error sending stream_end: {e}")

        except Exception as e:
            self.logger.error(f"Stream error for {request_id}: {e}")
            self.logger.exception(e)
            try:
                self.sio.emit(
                    "stream_end",
                    {"request_id": request_id, "error": str(e)},
                    namespace=self.namespace,
                )
            except Exception as emit_err:
                self.logger.error(f"Error sending stream_end after error: {emit_err}")

    def run(self):
        """Start the tunnel client."""
        self.logger.info(f"Starting AI Forward tunnel '{self.name}'")
        self.logger.info(f"Server: {self.server}")
        self.logger.info(f"Services: {', '.join(self.services)}")

        reconnect_delay = 1.0
        max_reconnect_delay = 30.0

        while self.running:
            try:
                self.logger.info(f"Connecting to {self._get_ws_url()}...")
                self.sio.connect(
                    self._get_connect_url(),
                    namespaces=[self.namespace],
                    wait_timeout=10,
                )
                reconnect_delay = 1.0
                self.sio.wait()

            except Exception as e:
                self.logger.error(f"Connection error: {e}")

            if self.running:
                self.logger.info(f"Reconnecting in {reconnect_delay}s...")
                time.sleep(reconnect_delay)
                reconnect_delay = min(reconnect_delay * 2, max_reconnect_delay)

    def stop(self):
        """Stop the tunnel client."""
        self.running = False
        if self.sio.connected:
            self.sio.disconnect()
        self.logger.info("Tunnel stopped")
