"""
Named Pipe Server for Windows
Handles bidirectional communication with Electron app via Windows Named Pipes

Features:
- Thread-safe client socket management
- Handshake protocol (hello_from_electron / hello_from_python)
- Automatic cleanup of stale connections
- Comprehensive logging
- Message queuing
- Length-prefixed JSON messages
"""

import sys
import os
import struct
import json
import threading
import queue
import time
import uuid
from typing import Optional, Dict, Any

# Windows-specific imports
if sys.platform == 'win32':
    import win32pipe
    import win32file
    import pywintypes
    import win32event
    import win32con
    import winerror
else:
    # Stub for non-Windows platforms (should not be imported on non-Windows)
    raise ImportError("pipe_server.py is only available on Windows")

from ws_package.log.writer import LogWriter
from ws_package.utils.error_codes import ErrorCode

logger = LogWriter.file(__name__)

# Named Pipe configuration
PIPE_NAME = r'\\.\pipe\workstatus_python_ipc'
PIPE_BUFFER_SIZE = 4096
PIPE_TIMEOUT = 5000  # 5 seconds in milliseconds
HANDSHAKE_TIMEOUT = 5.0  # 5 seconds to complete handshake


class PipeServer:
    """Named Pipe server for bidirectional communication with Electron"""
    
    def __init__(self, pipe_name: str = PIPE_NAME):
        self.pipe_name = pipe_name
        self.pipe_handle: Optional[int] = None
        self.client_handshake_complete = False
        self.session_id = str(uuid.uuid4())[:8]  # Short session ID for log correlation
        self.running = False
        self.pipe_created = threading.Event()  # Event to track when pipe is actually created
        self.message_queue = queue.Queue(maxsize=1000)
        self.receive_thread: Optional[threading.Thread] = None
        self.send_lock = threading.Lock()
        self.client_lock = threading.Lock()
        self.receive_buffer = b''
        self.accept_thread: Optional[threading.Thread] = None
        
    def start(self):
        """Start the Named Pipe server and listen for connections"""
        if self.running:
            logger.warning("Named Pipe server already running")
            return
        
        try:
            self.running = True
            
            # Print to stdout immediately (logger might not be ready)
            print(f"[Timing][Python] Named Pipe server starting on {self.pipe_name}", flush=True)
            logger.info("Named Pipe server starting on %s", self.pipe_name)
            
            # Start accept thread
            self.accept_thread = threading.Thread(
                target=self._accept_connections,
                daemon=True,
                name="PipeAcceptThread"
            )
            self.accept_thread.start()
            
            # Print confirmation
            print(f"[Timing][Python] Named Pipe server listening and ready for connections", flush=True)
            logger.info("Named Pipe server started and listening")
            
        except Exception as e:
            logger.critical("%s: %s", ErrorCode.IPC_PIPE_FAILED, e, exc_info=True)
            self.running = False
            raise
    
    def _accept_connections(self):
        """Accept incoming connections from Electron clients"""
        logger.info("Waiting for Electron client connection...")
        last_log_time = time.time()
        
        while self.running:
            try:
                # Check if we have an active client connection
                with self.client_lock:
                    has_active_client = (self.pipe_handle is not None and 
                                       self.client_handshake_complete)
                
                # Only create a new pipe instance if we don't have an active client
                # This prevents "All pipe instances are busy" error
                if has_active_client:
                    # We have an active client, just wait a bit and check again
                    time.sleep(0.5)
                    continue
                
                # Log every 30 seconds that we're still waiting
                current_time = time.time()
                if current_time - last_log_time >= 30.0:
                    logger.info("Still waiting for Electron client connection... (server running: %s)", self.running)
                    last_log_time = current_time
                
                # Create Named Pipe instance
                # PIPE_ACCESS_DUPLEX = 0x00000003
                # FILE_FLAG_OVERLAPPED = 0x40000000
                # PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT = 0x00000000 | 0x00000002 | 0x00000000
                try:
                    pipe_handle = win32pipe.CreateNamedPipe(
                        self.pipe_name,
                        win32pipe.PIPE_ACCESS_DUPLEX | win32file.FILE_FLAG_OVERLAPPED,
                        win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT,
                        1,  # Max instances
                        PIPE_BUFFER_SIZE,  # Out buffer size
                        PIPE_BUFFER_SIZE,  # In buffer size
                        PIPE_TIMEOUT,  # Timeout
                        None  # Security attributes
                    )
                except pywintypes.error as e:
                    if e.winerror == 231:  # ERROR_ALL_PIPE_INSTANCES_BUSY
                        # All pipe instances are busy - wait a bit and retry
                        logger.debug("All pipe instances busy, waiting before retry...")
                        time.sleep(0.5)
                        continue
                    else:
                        logger.error("%s: %s (winerror: %s)", ErrorCode.IPC_PIPE_FAILED, e, e.winerror)
                        time.sleep(1.0)
                        continue

                if pipe_handle == win32file.INVALID_HANDLE_VALUE:
                    logger.error("%s: CreateNamedPipe returned INVALID_HANDLE_VALUE — "
                                 "check antivirus Named Pipe rules", ErrorCode.IPC_PIPE_FAILED)
                    time.sleep(1.0)
                    continue
                
                logger.info("Named Pipe created, waiting for client connection...")
                
                # Signal that pipe is created (for is_ipc_ready check)
                self.pipe_created.set()
                
                # Store pipe handle BEFORE waiting for connection (so we know a pipe is waiting)
                with self.client_lock:
                    # Close any old pipe handle first
                    if self.pipe_handle and self.pipe_handle != pipe_handle:
                        try:
                            win32file.CloseHandle(self.pipe_handle)
                        except Exception:
                            pass
                    self.pipe_handle = pipe_handle
                    self.client_handshake_complete = False
                
                # Wait for client connection (non-blocking with timeout)
                overlapped = pywintypes.OVERLAPPED()
                overlapped.hEvent = win32event.CreateEvent(None, True, False, None)
                
                try:
                    # ConnectNamedPipe with overlapped I/O
                    # In overlapped mode, we always wait for the event regardless of return value
                    try:
                        result = win32pipe.ConnectNamedPipe(pipe_handle, overlapped)
                        # result can be:
                        # - 0 (FALSE): ERROR_IO_PENDING - operation is pending, wait for event
                        # - non-zero (TRUE): operation completed immediately (rare with overlapped)
                        # - Exception: actual error occurred
                        
                        # For overlapped I/O, always wait for the event to be signaled
                        # This ensures we properly handle both pending and immediate completions
                        wait_result = win32event.WaitForSingleObject(overlapped.hEvent, 60000)  # 60 seconds
                        
                        if wait_result == win32event.WAIT_TIMEOUT:
                            logger.warning("Connection timeout after 60s - closing pipe and will retry")
                            # Clear pipe handle and close
                            with self.client_lock:
                                if self.pipe_handle == pipe_handle:
                                    self.pipe_handle = None
                                    self.pipe_created.clear()  # Clear pipe created flag
                            win32file.CloseHandle(pipe_handle)
                            time.sleep(1.0)  # Wait before creating new pipe
                            continue
                        elif wait_result != win32event.WAIT_OBJECT_0:
                            logger.warning("Wait failed: %s", wait_result)
                            # Clear pipe handle and close
                            with self.client_lock:
                                if self.pipe_handle == pipe_handle:
                                    self.pipe_handle = None
                                    self.pipe_created.clear()  # Clear pipe created flag
                            win32file.CloseHandle(pipe_handle)
                            continue
                        
                        # Event was signaled - check if connection succeeded using GetOverlappedResult
                        try:
                            bytes_transferred = win32file.GetOverlappedResult(pipe_handle, overlapped, True)
                            # If we get here, connection succeeded
                            logger.info("Client connected via Named Pipe (overlapped I/O)")
                        except pywintypes.error as overlapped_error:
                            # Connection failed
                            logger.error("GetOverlappedResult failed: %s (winerror: %s)", overlapped_error, overlapped_error.winerror)
                            # Clear pipe handle and close
                            with self.client_lock:
                                if self.pipe_handle == pipe_handle:
                                    self.pipe_handle = None
                                    self.pipe_created.clear()  # Clear pipe created flag
                            win32file.CloseHandle(pipe_handle)
                            time.sleep(1.0)
                            continue
                            
                    except pywintypes.error as e:
                        # If client already connected, ConnectNamedPipe raises ERROR_PIPE_CONNECTED
                        if e.winerror == winerror.ERROR_PIPE_CONNECTED:
                            logger.info("Client already connected (ERROR_PIPE_CONNECTED)")
                            # Client is connected, continue with setup
                        else:
                            logger.error("ConnectNamedPipe error: %s (winerror: %s)", e, e.winerror)
                            # Clear pipe handle and close
                            with self.client_lock:
                                if self.pipe_handle == pipe_handle:
                                    self.pipe_handle = None
                                    self.pipe_created.clear()  # Clear pipe created flag
                            win32file.CloseHandle(pipe_handle)
                            time.sleep(1.0)
                            continue
                    
                    # Client connected - verify handle is still valid
                    with self.client_lock:
                        if self.pipe_handle != pipe_handle:
                            # Pipe was replaced, close this one
                            logger.warning("Pipe handle was replaced while waiting for connection")
                            win32file.CloseHandle(pipe_handle)
                            continue
                    
                    logger.info("Electron client connected via Named Pipe")
                    
                    # Initialize client state
                    with self.client_lock:
                        # Clean up old receive thread if exists
                        if self.receive_thread and self.receive_thread.is_alive():
                            logger.info("Old receive thread still running, will be replaced")
                        
                        # Ensure pipe_handle is set and initialize state
                        if self.pipe_handle == pipe_handle:
                            self.receive_buffer = b''
                            self.client_handshake_complete = False
                            current_handle = pipe_handle
                        else:
                            logger.error("ERROR: pipe_handle mismatch - cannot start receive thread!")
                            win32file.CloseHandle(pipe_handle)
                            continue
                    
                    # Start receive thread for this client
                    if current_handle:
                        logger.info("Starting receive thread for new client...")
                        self.receive_thread = threading.Thread(
                            target=self._receive_messages_with_handle,
                            args=(current_handle,),
                            daemon=True,
                            name="PipeReceiveThread"
                        )
                        self.receive_thread.start()
                        time.sleep(0.01)
                        logger.info("✓ Receive thread started, waiting for handshake...")
                    else:
                        logger.error("ERROR: current_handle is None - cannot start receive thread!")
                        win32file.CloseHandle(pipe_handle)
                        continue
                    
                    
                except Exception as e:
                    logger.error("Error accepting connection: %s", e, exc_info=True)
                    # Clear pipe handle on error
                    with self.client_lock:
                        if self.pipe_handle == pipe_handle:
                            self.pipe_handle = None
                            self.pipe_created.clear()  # Clear pipe created flag
                    try:
                        win32file.CloseHandle(pipe_handle)
                    except Exception:
                        pass
                    time.sleep(1.0)
                finally:
                    try:
                        win32event.CloseHandle(overlapped.hEvent)
                    except Exception:
                        pass
                
            except Exception as e:
                if self.running:
                    logger.error("Unexpected error in accept loop: %s", e, exc_info=True)
                time.sleep(1.0)
    
    def _receive_messages_with_handle(self, handle_ref: int):
        """Receive messages from client using captured handle reference (runs in separate thread)"""
        thread_name = threading.current_thread().name
        logger.info("Started receiving messages from Electron (thread: %s)", thread_name)
        handshake_timer_start = time.time()

        empty_read_count = 0
        MAX_CONSECUTIVE_EMPTY_READS = 10  # Protection against infinite empty read loops
        HANDSHAKE_TIMEOUT = 30  # 30 seconds for handshake

        while self.running:
            try:
                # Use captured handle reference to avoid race conditions
                logger.debug("Waiting to receive data from pipe (thread: %s)...", thread_name)
                
                # Create overlapped structure for this read
                overlapped_read = pywintypes.OVERLAPPED()
                overlapped_read.hEvent = win32event.CreateEvent(None, True, False, None)
                
                try:
                    # Read from pipe (overlapped read)
                    # In MESSAGE mode, ReadFile returns when a message is available
                    try:
                        # Pass overlapped structure to ReadFile
                        hr, data = win32file.ReadFile(handle_ref, PIPE_BUFFER_SIZE, overlapped_read)
                        # If ReadFile returns successfully (hr=0), operation completed immediately
                        # But with overlapped, it usually raises ERROR_IO_PENDING
                        
                    except pywintypes.error as e:
                        if e.winerror == 997:  # ERROR_IO_PENDING
                            # Operation pending - wait for event
                            # Wait for read to complete or timeout (check running flag periodically)
                            while self.running:
                                wait_result = win32event.WaitForSingleObject(overlapped_read.hEvent, 500)  # 500ms check
                                if wait_result == win32event.WAIT_OBJECT_0:
                                    # Read completed
                                    break
                                elif wait_result == win32event.WAIT_TIMEOUT:
                                    # Timeout - check if we should still be running
                                    continue
                                else:
                                    # Error waiting
                                    raise
                            
                            if not self.running:
                                break
                                
                            # Get result
                            bytes_read = win32file.GetOverlappedResult(handle_ref, overlapped_read, True)
                            # Read the data from the buffer (not directly returned by GetOverlappedResult in Python win32)
                            # We need to read the buffer we passed? No, win32file.ReadFile returns the buffer or None
                            # Actually in pywin32, ReadFile with overlapped returns (hr, buffer) immediately?
                            # No, for overlapped, we need to handle the buffer differently.
                            # Let's use the standard pattern: ReadFile returns immediately with IO_PENDING.
                            # But pywin32 ReadFile wrapper is tricky. 
                            # If we pass an overlapped object, it returns (err, data).
                            # If err is 0, data is valid. If err is 997, data is partial/invalid until overlapped completes?
                            # Actually, pywin32's ReadFile with overlapped returns (rc, buffer).
                            # If rc is 0, operation completed. If rc is 997, operation pending.
                            # When pending, we wait. After wait, we call GetOverlappedResult.
                            # BUT, how do we get the data?
                            # In pywin32, if we passed a buffer size (int), ReadFile allocates it.
                            # If we passed a buffer (mutable), it uses it.
                            # Since we passed PIPE_BUFFER_SIZE (int), it returns a buffer.
                            # But if it's pending, the buffer might be empty or incomplete.
                            # We need to rely on GetOverlappedResult to confirm.
                            # Wait, pywin32 ReadFile documentation says:
                            # "The result is a tuple of (hr, string/buffer), where hr may be 0 or ERROR_IO_PENDING"
                            # So 'data' variable holds the buffer.
                            pass
                        elif e.winerror == 109:  # ERROR_BROKEN_PIPE
                            logger.info("[Windows] Broken pipe - Electron client disconnected")
                            break
                        elif e.winerror == 233:  # ERROR_PIPE_NOT_CONNECTED
                            logger.info("[Windows] Pipe not connected - Electron client disconnected")
                            break
                        elif e.winerror == 234: # ERROR_MORE_DATA
                            logger.warning("[Windows] Message larger than buffer (ERROR_MORE_DATA) - partial read")
                            # We should handle this, but for now just continue
                            pass
                        else:
                            raise
                    
                    # If we are here, either ReadFile completed immediately or we waited for it
                    # Check if we actually got data
                    if not self.running:
                        break

                    # If we had pending IO, we need to ensure 'data' is valid.
                    # For pywin32, the 'data' returned by ReadFile might be valid after the wait?
                    # Actually, for overlapped read with buffer allocation (passing int), 
                    # we might need to use a mutable buffer to be safe, or rely on the returned data being populated?
                    # Let's verify pywin32 behavior.
                    # A safer way in pywin32 is to pass a pre-allocated buffer (AllocateReadBuffer).
                    # But let's try to use the returned 'data' if possible.
                    # Actually, if ReadFile raises ERROR_IO_PENDING, it returns (rc, buffer).
                    # The buffer object is returned. When the IO completes, the buffer is filled.
                    # So 'data' should be valid after WaitForSingleObject + GetOverlappedResult.
                    
                    # However, there is a catch: GetOverlappedResult returns the number of bytes transferred.
                    # We should slice the data to that length.
                    
                    bytes_transferred = win32file.GetOverlappedResult(handle_ref, overlapped_read, True)
                    
                    if bytes_transferred == 0:
                         # Empty read
                        empty_read_count += 1
                        if empty_read_count >= MAX_CONSECUTIVE_EMPTY_READS:
                            logger.info("Too many consecutive empty reads (%s) - Electron client likely disconnected", empty_read_count)
                            break
                        time.sleep(0.1)
                        continue
                    
                    # Slice data to actual bytes read
                    # Note: 'data' from ReadFile might be the full buffer size or just what was read?
                    # In pywin32, it returns a string/bytes of the size requested usually?
                    # Let's assume we need to slice it.
                    message_json = data[:bytes_transferred]
                    
                    # Reset empty read counter on successful read
                    empty_read_count = 0
                    
                    logger.info("✓ Received %s bytes from Electron (thread: %s)", len(message_json), thread_name)
                    
                    # In MESSAGE mode, we treat the read data as a complete message
                    # No buffering or length prefix parsing needed
                    
                    try:
                        # Decode message - handle both UTF-8 and potential null bytes
                        try:
                            # Ensure we have bytes (ReadFile with overlapped might return memoryview)
                            if isinstance(message_json, memoryview):
                                message_bytes = message_json.tobytes()
                            elif isinstance(message_json, (bytes, bytearray)):
                                message_bytes = message_json
                            else:
                                # Fallback for other types (e.g. str? unlikely from ReadFile)
                                message_bytes = bytes(message_json)
                                
                            message_str = message_bytes.decode('utf-8', errors='replace').strip('\x00\n\r\t ')
                            logger.info("[Windows Debug] Decoded string (len=%s): '%s'", len(message_str), message_str)
                        except Exception as decode_err:
                            logger.warning("[Windows Error] Failed to decode message bytes: %s", decode_err)
                            continue
                        
                        # Skip empty messages
                        if not message_str:
                            continue
                        
                        # Check if this is a plain text idle popup response
                        if message_str.startswith('keep:'):
                            logger.info("[Windows Debug] Detected idle popup response format")
                            # Parse plain text format: "keep:1 continue" or "keep:0 stop"
                            try:
                                parts = message_str.split(' ', 1)
                                keep_part = parts[0]  # "keep:1" or "keep:0"
                                action = parts[1] if len(parts) > 1 else 'continue'  # "continue", "stop", etc.
                                
                                keep_value = keep_part.split(':')[1]  # "1" or "0"
                                keep_idle_time = (keep_value == '1')
                                
                                # Convert to JSON message format
                                message = {
                                    'type': 'idle_popup_response',
                                    'keep_idle_time': keep_idle_time,
                                    'action': action
                                }
                                message_type = 'idle_popup_response'
                                logger.info("✓ Received idle popup response from Electron: keep_idle_time=%s, action=%s", keep_idle_time, action)
                            except (ValueError, IndexError) as e:
                                logger.warning("[Windows Error] Failed to parse idle popup response format: %s - %s", message_str, e)
                                continue
                        else:
                            message = json.loads(message_str)
                            
                            # Validate message structure
                            if not isinstance(message, dict):
                                logger.warning("[Windows Error] Invalid message structure: expected dict, got %s", type(message))
                                continue
                            
                            message_type = message.get('type', 'unknown')
                            logger.info("✓ Received message from Electron: type=%s", message_type)
                        
                        # Handle handshake
                        if message_type == 'hello_from_electron':
                            from ws_package.utils.ipc_session import (
                                verify_electron_handshake,
                                make_python_handshake_response,
                            )
                            accepted, secure = verify_electron_handshake(message)
                            if not accepted:
                                logger.error("IPC handshake auth failed — disconnecting client")
                                break
                            logger.info("✓ Handshake received: hello_from_electron")
                            # Send handshake response
                            with self.client_lock:
                                if self.pipe_handle is handle_ref:
                                    try:
                                        handshake_response = make_python_handshake_response(self.session_id, secure)
                                        self._send_message_internal(handle_ref, handshake_response)
                                        self.client_handshake_complete = True
                                        logger.info("✓ Handshake complete - sent hello_from_python")

                                        # Drop messages queued while disconnected so start/stop are not
                                        # stuck behind stale pings after Electron reconnects.
                                        stale_cleared = 0
                                        while True:
                                            try:
                                                self.message_queue.get_nowait()
                                                stale_cleared += 1
                                            except queue.Empty:
                                                break
                                        if stale_cleared:
                                            logger.info(
                                                "Cleared %s stale IPC message(s) after reconnect handshake",
                                                stale_cleared,
                                            )
                                        
                                        # Send ready signal after handshake
                                        time.sleep(0.1)
                                        try:
                                            from ws_package.ws_engine import PYTHON_APP_VERSION
                                            ready_message = {
                                                "type": "ready",
                                                "data": {
                                                    "status": "ready",
                                                    "message": "WS_Engine ready (Named Pipe connected)",
                                                    "capabilities": ["set_project_task", "stop"],
                                                    "version": PYTHON_APP_VERSION
                                                }
                                            }
                                            self._send_message_internal(handle_ref, ready_message)
                                            logger.info("✓ Sent 'ready' signal to Electron after handshake")
                                        except Exception as e:
                                            logger.error("Failed to send ready signal after handshake: %s", e, exc_info=True)
                                    except Exception as e:
                                        logger.error("Error sending handshake response: %s", e, exc_info=True)
                                        break
                            continue
                        
                        # Check handshake timeout
                        if not self.client_handshake_complete:
                            elapsed = time.time() - handshake_timer_start
                            logger.warning("[Windows Debug] Handshake not complete yet (elapsed: %ss)", elapsed)
                            if elapsed > HANDSHAKE_TIMEOUT:
                                logger.error("[Windows Error] Handshake timeout after %ss - disconnecting client", elapsed)
                                break
                        
                        # Queue regular messages
                        try:
                            self.message_queue.put_nowait(message)
                            logger.info("✓ Queued message: type=%s, queue_size=%s", message_type, self.message_queue.qsize())
                        except queue.Full:
                            logger.warning("Message queue full (size=%s), dropping message: type=%s", self.message_queue.maxsize, message_type)
                        
                    except json.JSONDecodeError as e:
                        preview = message_str[:50] if 'message_str' in locals() else '<not decoded>'
                        hex_preview = message_json[:20].hex() if len(message_json) <= 20 else message_json[:20].hex() + '...'
                        logger.warning("[Windows Error] Invalid JSON (length=%s): preview='%s', hex=%s, error=%s", len(message_json), preview, hex_preview, e)
                    except UnicodeDecodeError as e:
                        hex_preview = message_json[:20].hex() if len(message_json) <= 20 else message_json[:20].hex() + '...'
                        logger.warning("[Windows Error] Invalid encoding (length=%s): hex=%s, error=%s", len(message_json), hex_preview, e)
                    except Exception as e:
                        # Catch any other unexpected errors
                        logger.error("[Windows Error] Unexpected error processing message: %s", e, exc_info=True)
                
                except pywintypes.error as e:
                    # Use numeric error codes instead of win32file constants for PyInstaller compatibility
                    # ERROR_BROKEN_PIPE = 109, ERROR_PIPE_NOT_CONNECTED = 233, ERROR_IO_PENDING = 997, ERROR_MORE_DATA = 234
                    if e.winerror == 109:  # ERROR_BROKEN_PIPE
                        logger.info("[Windows] Broken pipe - Electron client disconnected")
                        break
                    elif e.winerror == 233:  # ERROR_PIPE_NOT_CONNECTED
                        logger.info("[Windows] Pipe not connected - Electron client disconnected")
                        break
                    elif e.winerror == 997:  # ERROR_IO_PENDING
                        # Should be handled in inner try, but if it bubbles up:
                        pass
                    else:
                        if self.running:
                            logger.error("[Windows Error] Pipe error receiving message: %s (winerror: %s)", e, e.winerror, exc_info=True)
                        break
                finally:
                    # Cleanup event handle
                    if overlapped_read.hEvent:
                        try:
                            win32event.CloseHandle(overlapped_read.hEvent)
                        except Exception:
                            pass
                
            except Exception as e:
                if self.running:
                    logger.error("Error receiving message: %s", e, exc_info=True)
                break
        
        # Clean up client connection only if this handle is still the active one
        should_disconnect = False
        with self.client_lock:
            if self.pipe_handle is handle_ref:
                should_disconnect = True
        
        if should_disconnect:
            self._disconnect_client()
        logger.warning("Stopped receiving messages from Electron")
    
    def _disconnect_client(self):
        """Disconnect current client"""
        with self.client_lock:
            if self.pipe_handle:
                try:
                    win32file.CloseHandle(self.pipe_handle)
                    logger.info("Closed client pipe handle")
                except Exception as e:
                    logger.warning("Error closing client pipe handle: %s", e)
                self.pipe_handle = None
                self.client_handshake_complete = False
                self.receive_buffer = b''
                # Don't clear pipe_created here - pipe will be recreated in accept loop
                logger.info("Electron client disconnected, waiting for reconnection...")
    
    def _send_message_internal(self, handle: int, message: Dict[str, Any]):
        """Internal method to send a message (length-prefixed)"""
        if not handle:
            raise ValueError("Invalid handle: handle is None or 0")
        if not isinstance(message, dict):
            raise ValueError(f"Invalid message type: expected dict, got {type(message)}")
        
        # Serialize to JSON
        try:
            message_str = json.dumps(message)
            message_bytes = message_str.encode('utf-8')
        except (TypeError, ValueError) as e:
            raise ValueError(f"Failed to serialize message to JSON: {e}")

        MAX_MESSAGE_SIZE = 10 * 1024 * 1024  # 10MB max
        if len(message_bytes) > MAX_MESSAGE_SIZE:
            raise ValueError(f"Message too large: {len(message_bytes)} bytes (max={MAX_MESSAGE_SIZE})")
        
        # Create message bytes
        # In MESSAGE mode, we just send the bytes directly
        # No length prefix needed as the OS preserves message boundaries
        
        # Write to pipe
        win32file.WriteFile(handle, message_bytes)
    
    def send_message(self, message_type: str, data: Optional[Dict[str, Any]] = None) -> bool:
        """
        Send message to Electron client
        
        Args:
            message_type: Type of message (e.g., 'idle_popup_trigger', 'activity_update')
            data: Optional data dictionary to send
            
        Returns:
            True if sent successfully, False otherwise
        """
        # Capture handle reference to avoid race conditions
        with self.client_lock:
            handle_ref = self.pipe_handle
            handshake_complete = self.client_handshake_complete
        
        if not handle_ref:
            logger.debug("Cannot send message - no client connected: %s", message_type)
            return False
        
        if not handshake_complete:
            logger.debug("Cannot send message - handshake not complete: %s", message_type)
            return False
        
        message = {
            "type": message_type,
            "data": data
        }
        
        try:
            with self.send_lock:
                self._send_message_internal(handle_ref, message)
                logger.info("✓ Sent message to Electron: %s", message_type)
                return True
                
        except pywintypes.error as e:
            # Use numeric error codes instead of win32file constants for PyInstaller compatibility
            # ERROR_BROKEN_PIPE = 109, ERROR_PIPE_NOT_CONNECTED = 233
            if e.winerror == 109:  # ERROR_BROKEN_PIPE
                logger.warning("[Windows] Broken pipe while sending - client disconnected")
                self._disconnect_client()
            elif e.winerror == 233:  # ERROR_PIPE_NOT_CONNECTED
                logger.warning("[Windows] Pipe not connected while sending - client disconnected")
                self._disconnect_client()
            else:
                logger.error("[Windows Error] Pipe error sending message: %s (winerror: %s)", e, e.winerror, exc_info=True)
                self._disconnect_client()
            return False
        except Exception as e:
            logger.error("Error sending message to Electron: %s", e, exc_info=True)
            return False
    
    def get_message(self, timeout: Optional[float] = None) -> Optional[Dict[str, Any]]:
        """
        Get a message from Electron (non-blocking or with timeout)
        
        Args:
            timeout: Timeout in seconds. If None, returns immediately if no message.
                     If 0, blocks indefinitely until message arrives.
        
        Returns:
            Message dictionary or None if timeout/empty
        """
        try:
            if timeout is None:
                return self.message_queue.get_nowait()
            else:
                return self.message_queue.get(timeout=timeout)
        except queue.Empty:
            return None
    
    def is_connected(self) -> bool:
        """
        Check if client is actually connected and handshake complete.
        Verifies the pipe handle is still valid.
        Non-destructive check - does not disconnect on failure.
        """
        with self.client_lock:
            handle_ref = self.pipe_handle
            handshake_complete = self.client_handshake_complete
        
        if not handle_ref or not handshake_complete:
            return False
        
        # Verify pipe is actually connected by attempting a peek
        try:
            win32pipe.PeekNamedPipe(handle_ref, 0)
            return True
        except pywintypes.error:
            logger.debug("Pipe handle exists but is not connected (PeekNamedPipe failed)")
            return False
    
    def stop(self):
        """Stop the Named Pipe server"""
        if not self.running:
            return
        
        logger.info("Stopping Named Pipe server...")
        self.running = False
        self.pipe_created.clear()  # Clear pipe created flag
        
        # Disconnect client
        self._disconnect_client()
        
        time.sleep(0.1)
        
        logger.info("Named Pipe server stopped")


# Global instance
_pipe_server: Optional[PipeServer] = None


def get_pipe_server() -> PipeServer:
    """Get the global Named Pipe server instance"""
    global _pipe_server
    if _pipe_server is None:
        _pipe_server = PipeServer()
    return _pipe_server


def start_pipe_server():
    """Start the Named Pipe server (call this at startup)"""
    server = get_pipe_server()
    server.start()


def stop_pipe_server():
    """Stop the Named Pipe server (call this at shutdown)"""
    global _pipe_server
    if _pipe_server:
        _pipe_server.stop()
        _pipe_server = None
