"""
STARWASTE - Client Network Handler
Async networking for client-server communication
"""

import asyncio
import msgpack
import struct
import threading
import os
import random
import time
import heapq
from typing import Optional, Callable
from queue import Queue
from shared.utils import console
from shared.protocol import Protocol


# ---------------------------------------------------------------------------
# NetSim (debug): simple network lag simulation
#
# Enable by editing this file (no UI):
# - Set NETSIM_ENABLED = True
# - Adjust IN/OUT latency, jitter, and optional drop.
#
# Environment variables still work as an optional override when
# NETSIM_ENV_OVERRIDE = True.
# ---------------------------------------------------------------------------
NETSIM_ENABLED = False
# If True, allow environment variables (STARWASTE_NET_SIM_*) to override the values below.
# This is NOT related to any .env file; it's normal OS environment variables.
NETSIM_ENV_OVERRIDE = False

# All values are in milliseconds, drop probabilities are 0.0-1.0
NETSIM_IN_MS = 0.0
NETSIM_IN_JITTER_MS = 0.0
NETSIM_OUT_MS = 0.0
NETSIM_OUT_JITTER_MS = 0.0
NETSIM_DROP_IN_P = 0.0
NETSIM_DROP_OUT_P = 0.0


class NetworkClient:
    """Client network handler"""
    
    def __init__(self):
        self.reader: Optional[asyncio.StreamReader] = None
        self.writer: Optional[asyncio.StreamWriter] = None
        self.connected = False
        self.player_id: Optional[int] = None
        self.username: str = ""
        self.message_callback: Optional[Callable] = None
        self.send_queue: Queue = Queue()
        self.loop: Optional[asyncio.AbstractEventLoop] = None
        
        # Ping/latency tracking
        self.ping_ms = 0
        self.last_ping_time = 0.0

        # NetSim incoming scheduling queue: (release_time_monotonic, seq, message)
        self._net_sim_in_queue = []
        self._net_sim_in_seq = 0

        # Optional network simulation (latency/jitter/drop).
        # Primary control is via the constants at the top of this file.
        # NETSIM_ENABLED is a master switch to prevent accidental enablement.
        # Environment variables can override values when NETSIM_ENV_OVERRIDE=True.
        if NETSIM_ENABLED:
            self._net_sim_in_ms = float(NETSIM_IN_MS or 0)
            self._net_sim_in_jitter_ms = float(NETSIM_IN_JITTER_MS or 0)
            self._net_sim_out_ms = float(NETSIM_OUT_MS or 0)
            self._net_sim_out_jitter_ms = float(NETSIM_OUT_JITTER_MS or 0)
            self._net_sim_drop_in_p = float(NETSIM_DROP_IN_P or 0)
            self._net_sim_drop_out_p = float(NETSIM_DROP_OUT_P or 0)
        else:
            self._net_sim_in_ms = 0.0
            self._net_sim_in_jitter_ms = 0.0
            self._net_sim_out_ms = 0.0
            self._net_sim_out_jitter_ms = 0.0
            self._net_sim_drop_in_p = 0.0
            self._net_sim_drop_out_p = 0.0

        if NETSIM_ENABLED and NETSIM_ENV_OVERRIDE:
            # Env overrides (useful for one-off testing without editing files)
            self._net_sim_in_ms = float(os.getenv("STARWASTE_NET_SIM_IN_MS", str(self._net_sim_in_ms)) or 0)
            self._net_sim_in_jitter_ms = float(os.getenv("STARWASTE_NET_SIM_IN_JITTER_MS", str(self._net_sim_in_jitter_ms)) or 0)
            self._net_sim_out_ms = float(os.getenv("STARWASTE_NET_SIM_OUT_MS", str(self._net_sim_out_ms)) or 0)
            self._net_sim_out_jitter_ms = float(os.getenv("STARWASTE_NET_SIM_OUT_JITTER_MS", str(self._net_sim_out_jitter_ms)) or 0)
            self._net_sim_drop_in_p = float(os.getenv("STARWASTE_NET_SIM_DROP_IN_P", str(self._net_sim_drop_in_p)) or 0)
            self._net_sim_drop_out_p = float(os.getenv("STARWASTE_NET_SIM_DROP_OUT_P", str(self._net_sim_drop_out_p)) or 0)

    def _net_sim_enabled(self) -> bool:
        return any(
            v > 0.0
            for v in (
                self._net_sim_in_ms,
                self._net_sim_in_jitter_ms,
                self._net_sim_out_ms,
                self._net_sim_out_jitter_ms,
                self._net_sim_drop_in_p,
                self._net_sim_drop_out_p,
            )
        )

    async def _net_sim_delay(self, *, direction: str):
        """Artificial delay for incoming/outgoing network paths."""
        if direction == "in":
            base_ms = self._net_sim_in_ms
            jitter_ms = self._net_sim_in_jitter_ms
        else:
            base_ms = self._net_sim_out_ms
            jitter_ms = self._net_sim_out_jitter_ms

        if base_ms <= 0.0 and jitter_ms <= 0.0:
            return

        delay_ms = float(base_ms)
        if jitter_ms > 0.0:
            delay_ms += random.uniform(-jitter_ms, jitter_ms)
        if delay_ms < 0.0:
            delay_ms = 0.0

        await asyncio.sleep(delay_ms / 1000.0)

    def _net_sim_delay_seconds(self, *, direction: str) -> float:
        """Compute artificial delay for incoming/outgoing paths without awaiting."""
        if direction == "in":
            base_ms = self._net_sim_in_ms
            jitter_ms = self._net_sim_in_jitter_ms
        else:
            base_ms = self._net_sim_out_ms
            jitter_ms = self._net_sim_out_jitter_ms

        if base_ms <= 0.0 and jitter_ms <= 0.0:
            return 0.0

        delay_ms = float(base_ms)
        if jitter_ms > 0.0:
            delay_ms += random.uniform(-jitter_ms, jitter_ms)
        if delay_ms < 0.0:
            delay_ms = 0.0
        return delay_ms / 1000.0

    def _net_sim_should_drop(self, *, direction: str, message_type: str | None) -> bool:
        """Drop decision for simulated packet loss.

        We never drop the initial "welcome" flow (handled elsewhere) and we avoid dropping
        ping/pong by default because it makes latency readouts confusing.
        """
        if message_type in ("ping", "pong", "welcome"):
            return False

        p = self._net_sim_drop_in_p if direction == "in" else self._net_sim_drop_out_p
        if p <= 0.0:
            return False
        if p >= 1.0:
            return True
        return random.random() < p
        
    async def connect(self, host: str, port: int, username: str) -> bool:
        """
        Connect to server
        
        Args:
            host: Server host
            port: Server port
            username: Player username
        
        Returns:
            True if connected successfully
        """
        try:
            # Store event loop
            self.loop = asyncio.get_running_loop()
            
            console.print(f"🔌 Connecting to {host}:{port}...")

            if self._net_sim_enabled():
                console.print(
                    "🧪 NetSim enabled: "
                    f"in={self._net_sim_in_ms:.0f}±{self._net_sim_in_jitter_ms:.0f}ms "
                    f"out={self._net_sim_out_ms:.0f}±{self._net_sim_out_jitter_ms:.0f}ms "
                    f"drop_in={self._net_sim_drop_in_p:.2f} drop_out={self._net_sim_drop_out_p:.2f}"
                )
            
            self.reader, self.writer = await asyncio.open_connection(host, port)
            
            # Send player info (length-prefixed MessagePack)
            player_info = {"username": username}
            packed = msgpack.packb(player_info, use_bin_type=True)
            self.writer.write(packed)
            await self.writer.drain()

            # Store username
            self.username = username

            # Wait for welcome message (length-prefixed)
            # Use streaming Unpacker to handle raw MessagePack stream
            unpacker = Protocol.create_unpacker(raw=False)
            # Read until we get at least one object
            while True:
                chunk = await asyncio.wait_for(self.reader.read(4096), timeout=5.0)
                if not chunk:
                    raise ConnectionError("No data received for welcome")
                unpacker.feed(chunk)
                for obj in unpacker:
                    welcome = obj
                    break
                if 'welcome' in locals():
                    break
            
            if welcome.get("type") == "welcome":
                self.player_id = welcome.get("player_id")
                self.connected = True
                console.print(f"✅ Connected! Player ID: {self.player_id}")
                
                # Pass welcome message to callback for spawn position
                if self.message_callback:
                    self.message_callback(welcome)
                
                return True
            else:
                console.print(f"⚠️ Unexpected welcome message: {welcome}")
                return False
            
        except asyncio.TimeoutError:
            console.print(f"❌ Connection timeout - server not responding")
            return False
        except ConnectionRefusedError:
            console.print(f"❌ Connection refused - is server running?")
            return False
        except Exception as e:
            console.print(f"❌ Connection failed: {e}")
            import traceback
            traceback.print_exc()
            return False
    
    async def disconnect(self):
        """Disconnect from server"""
        if self.writer:
            self.writer.close()
            await self.writer.wait_closed()
        
        self.connected = False
        console.print("🔌 Disconnected")
    
    async def send_message(self, message: dict):
        """Send message to server"""
        if not self.connected or not self.writer:
            return
        
        try:
            msg_type = None
            try:
                msg_type = message.get("type")
            except Exception:
                msg_type = None

            if self._net_sim_should_drop(direction="out", message_type=msg_type):
                return
            await self._net_sim_delay(direction="out")

            packed = msgpack.packb(message, use_bin_type=True)
            self.writer.write(packed)
            await self.writer.drain()
        except Exception as e:
            console.print(f"❌ Send error: {e}")
            self.connected = False
    
    async def receive_loop(self):
        """Receive messages from server"""
        console.print("🔄 Network receive loop started")
        # Use a single streaming Unpacker across reads so partial messages
        # are retained until complete. Recreating the Unpacker each read
        # can drop partial bytes and produce spurious unpacked primitives.
        unpacker = Protocol.create_unpacker(raw=False)

        def _dispatch_message(msg: dict):
            # Track ping for pong responses
            if msg.get("type") == "pong":
                self.ping_ms = int((time.time() - self.last_ping_time) * 1000)

            if self.message_callback:
                try:
                    self.message_callback(msg)
                except Exception as e:
                    console.print(f"⚠️ Callback error: {e}")

        while self.connected and self.reader:
            try:
                # Dispatch any delayed incoming messages that are due.
                if self._net_sim_in_queue:
                    now_mono = time.monotonic()
                    while self._net_sim_in_queue and self._net_sim_in_queue[0][0] <= now_mono:
                        _, _, due_msg = heapq.heappop(self._net_sim_in_queue)
                        _dispatch_message(due_msg)

                # Process send queue
                while not self.send_queue.empty():
                    try:
                        msg = self.send_queue.get_nowait()
                        await self.send_message(msg)
                    except Exception as e:
                        console.print(f"⚠️ Send queue error: {e}")
                
                # Check for incoming messages (non-blocking)
                try:
                    # Use streaming Unpacker to read any available messages
                    try:
                        timeout = 0.1
                        if self._net_sim_in_queue:
                            # Wake up in time to dispatch scheduled incoming messages.
                            until_due = self._net_sim_in_queue[0][0] - time.monotonic()
                            if until_due < 0.0:
                                until_due = 0.0
                            timeout = min(timeout, until_due)
                        chunk = await asyncio.wait_for(self.reader.read(4096), timeout=timeout)
                    except asyncio.TimeoutError:
                        # If we have scheduled messages, loop so we can dispatch them ASAP.
                        if self._net_sim_in_queue:
                            continue
                        await asyncio.sleep(0.01)
                        continue
                    if not chunk:
                        console.print("⚠️ Connection closed by server")
                        break
                    unpacker.feed(chunk)
                    for message in unpacker:
                        msg_type = None
                        try:
                            msg_type = message.get("type")
                        except Exception:
                            msg_type = None

                        if self._net_sim_should_drop(direction="in", message_type=msg_type):
                            continue

                        delay_s = self._net_sim_delay_seconds(direction="in")
                        if delay_s <= 0.0:
                            _dispatch_message(message)
                        else:
                            release_at = time.monotonic() + delay_s
                            self._net_sim_in_seq += 1
                            heapq.heappush(self._net_sim_in_queue, (release_at, self._net_sim_in_seq, message))
                            
                except asyncio.TimeoutError:
                    # No message received, continue
                    await asyncio.sleep(0.01)
                
            except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError) as e:
                console.print(f"🔌 Connection lost: {e}")
                break
            except Exception as e:
                console.print(f"❌ Receive loop error: {e}")
                # Don't break - try to continue
                await asyncio.sleep(0.1)
        
        self.connected = False
        console.print("🔌 Network receive loop ended")
    
    def send_move(self, vx: float, vy: float):
        """Send movement update (thread-safe)"""
        message = {
            "type": "move",
            "vx": vx,
            "vy": vy
        }
        self.send_queue.put(message)
    
    def send_chat(self, message: str):
        """Send chat message (thread-safe)"""
        self.send_queue.put({
            "type": "chat",
            "message": message
        })
    
    def send_ping(self):
        """Send ping to measure latency (thread-safe)"""
        # Measure from the time the ping is requested (includes send queue + any NetSim out delay)
        self.last_ping_time = time.time()
        self.send_queue.put({
            "type": "ping"
        })
    
    def send_harvest_start(self, resource_id: int):
        """Send harvest start request (thread-safe)"""
        self.send_queue.put({
            "type": "harvest_start",
            "resource_id": resource_id
        })
    
    def send_chunk_request(self, chunk_list: list):
        """Send chunk request to server (thread-safe)"""
        self.send_queue.put({
            "type": "chunk_request",
            "chunks": chunk_list
        })

    def send_pickup_item(self, entity_id: int):
        """Request to pick up an item entity (thread-safe)."""
        self.send_queue.put({
            "type": "pickup_item",
            "entity_id": int(entity_id)
        })

    def send_inventory_move(self, *, src_type: str, src_slot, dst_type: str, dst_slot):
        """Request inventory move/equip action (thread-safe)."""
        self.send_queue.put({
            "type": "inventory_move",
            "src_type": src_type,
            "src_slot": src_slot,
            "dst_type": dst_type,
            "dst_slot": dst_slot,
        })

    def send_drop_item(self, src_type: str, src_slot, *, facing_dx: float = 0.0, facing_dy: float = 0.0):
        """Request dropping a stack from inventory/equipment to the ground."""
        self.send_queue.put({
            "type": "drop_item",
            "src_type": src_type,
            "src_slot": src_slot,
            "facing_dx": float(facing_dx),
            "facing_dy": float(facing_dy),
        })
    
    def set_message_callback(self, callback: Callable):
        """Set callback for received messages"""
        self.message_callback = callback
