"""WebSocket client for playing matches."""

import asyncio
import json
from typing import Callable, Optional

import websockets

from .agent_loader import AgentBase
from .config import get_token, get_ws_url


class MatchClient:
    """WebSocket client for playing a match."""

    def __init__(
        self,
        agent: AgentBase,
        on_message: Optional[Callable[[str], None]] = None,
        game_id: str = "split-or-steal",
    ):
        self.agent = agent
        self.on_message = on_message or (lambda x: None)
        self.ws_url = get_ws_url()
        self.token = get_token()
        self.match_id: Optional[int] = None
        self.match_result: Optional[dict] = None
        self.game_id = game_id

    async def play(self) -> dict:
        """
        Connect and play a full match.

        Returns the match result dict.
        """
        url = f"{self.ws_url}/play?token={self.token}&game={self.game_id}"

        try:
            async with websockets.connect(url) as ws:
                self.ws = ws

                # Wait for connection confirmation
                response = await self._receive()
                if response.get("type") == "connected":
                    self.on_message(f"Connected as {response.get('agent_name')}")
                else:
                    raise Exception(f"Unexpected response: {response}")

                # Start the match
                await self._send({"type": "start_match"})

                # Main loop - handle messages until match ends
                while True:
                    response = await self._receive()
                    should_continue = await self._handle_message(response)
                    if not should_continue:
                        break

                return self.match_result or {}

        except websockets.exceptions.ConnectionClosedError as e:
            if e.code == 4001:
                raise Exception("Authentication failed. Please run 'agentduel login' first.")
            elif e.code == 4002:
                raise Exception("No agent found. Please create an agent on the web first.")
            elif e.code == 4003:
                raise Exception("Rate limit exceeded. Maximum 20 training matches per day.")
            else:
                raise Exception(f"Connection closed: {e}")

    async def _send(self, data: dict):
        """Send a message to the server."""
        await self.ws.send(json.dumps(data))

    async def _receive(self) -> dict:
        """Receive a message from the server."""
        text = await self.ws.recv()
        return json.loads(text)

    async def _handle_message(self, msg: dict) -> bool:
        """
        Handle a message from the server.

        Returns True to continue, False to stop.
        """
        msg_type = msg.get("type")

        if msg_type == "match_started":
            self.match_id = msg.get("match_id")
            game_num = msg.get("game_number", 1)
            position = msg.get("your_position", "unknown")
            self.on_message(f"\nMatch #{self.match_id} started!")
            self.on_message(f"Game {game_num} - You go {position}")
            return True

        elif msg_type == "game_start":
            # Print game header for games 2+
            game_num = msg.get("game_number", 1)
            if game_num > 1:
                position = msg.get("position", "unknown")
                self.on_message(f"\nGame {game_num} - You go {position}")

            # Notify agent
            if hasattr(self.agent, "on_game_start"):
                self.agent.on_game_start(msg)
            return True

        elif msg_type == "your_turn":
            state = msg.get("state", {})
            # Extract game_state for the agent - it contains the actual game data
            game_state = state.get("game_state", state)
            phase = game_state.get("phase", "unknown")

            # Get action from agent - pass game_state, not the wrapper
            try:
                action = self.agent.on_turn(game_state)
            except Exception as e:
                self.on_message(f"Agent error: {e}")
                # Use default action based on game type
                if self.game_id == "liars-dice":
                    # Default: make a conservative bid or challenge
                    action = {"type": "challenge"}
                elif phase == "negotiate":
                    action = {"type": "message", "text": ""}
                else:
                    action = {"type": "commit", "choice": "split"}

            # Send action (format depends on game type)
            if self.game_id == "liars-dice":
                if action.get("type") == "bid":
                    await self._send({
                        "type": "action",
                        "action_type": "bid",
                        "quantity": action.get("quantity", 1),
                        "face": action.get("face", 2),
                    })
                    self.on_message(f"  Your bid: {action.get('quantity')} x {action.get('face')}s")
                else:
                    await self._send({
                        "type": "action",
                        "action_type": "challenge",
                    })
                    self.on_message("  You called: LIAR!")
            elif action.get("type") == "message":
                await self._send({
                    "type": "action",
                    "action_type": "message",
                    "text": action.get("text", ""),
                })
                self.on_message(f"  You: {action.get('text', '')}")
            else:
                await self._send({
                    "type": "action",
                    "action_type": "commit",
                    "choice": action.get("choice", "split"),
                })
                self.on_message(f"  Your commit: {action.get('choice', 'split').upper()}")

            return True

        elif msg_type == "opponent_message":
            text = msg.get("text", "")
            self.on_message(f"  Opponent: {text}")
            return True

        elif msg_type == "opponent_action":
            # Handle opponent action (used in Liar's Dice)
            action = msg.get("action", {})
            action_type = action.get("action_type", "unknown")
            if action_type == "bid":
                qty = action.get("quantity", "?")
                face = action.get("face", "?")
                self.on_message(f"  Opponent bid: {qty} x {face}s")
            elif action_type == "challenge":
                self.on_message("  Opponent called: LIAR!")
            return True

        elif msg_type == "action_accepted":
            return True

        elif msg_type == "timeout":
            self.on_message(f"  ⚠ {msg.get('message', 'Timeout')}")
            return True

        elif msg_type == "game_end":
            game_num = msg.get("game_number", 0)
            your_pts = msg.get("your_points", 0)
            opp_pts = msg.get("opponent_points", 0)

            self.on_message(f"\n  Game {game_num} Result:")

            if self.game_id == "liars-dice":
                # Liar's Dice specific output
                result = msg.get("result", {})
                if result:
                    challenger = "You" if result.get("challenger") == "player_a" else "Opponent"
                    bid = result.get("bid", {})
                    actual = result.get("actual_count", 0)
                    bid_valid = result.get("bid_was_valid", False)
                    self.on_message(f"    {challenger} called LIAR!")
                    self.on_message(f"    Bid was: {bid.get('quantity', '?')} x {bid.get('face', '?')}s")
                    self.on_message(f"    Actual count: {actual}")
                    self.on_message(f"    Bid was {'valid' if bid_valid else 'a lie'}!")
                self.on_message(f"    Points: You {your_pts} - Opponent {opp_pts}")
            else:
                # Split or Steal output
                your_choice = msg.get("your_choice", "?")
                opp_choice = msg.get("opponent_choice", "?")
                self.on_message(f"    You: {your_choice.upper()} → {your_pts} pts")
                self.on_message(f"    Opponent: {opp_choice.upper()} → {opp_pts} pts")

            self.on_message(f"    Running score: {msg.get('your_total_score', 0)} - {msg.get('opponent_total_score', 0)}")

            # Notify agent
            if hasattr(self.agent, "on_game_end"):
                self.agent.on_game_end(msg)

            return True

        elif msg_type == "match_end":
            self.match_result = msg
            your_total = msg.get("your_total_score", 0)
            opp_total = msg.get("opponent_total_score", 0)
            winner = msg.get("winner", "unknown")

            self.on_message(f"\n{'='*40}")
            self.on_message(f"MATCH COMPLETE!")
            self.on_message(f"Final Score: {your_total} - {opp_total}")

            if winner == "you":
                self.on_message("Result: YOU WIN!")
            elif winner == "draw":
                self.on_message("Result: DRAW")
            else:
                self.on_message("Result: You lose")

            self.on_message(f"\nView match: http://localhost:3000/matches/{self.match_id}")
            self.on_message(f"{'='*40}")

            return False

        elif msg_type == "error":
            self.on_message(f"Error: {msg.get('message', 'Unknown error')}")
            return True

        else:
            self.on_message(f"Unknown message type: {msg_type}")
            return True


def play_match(
    agent: AgentBase,
    on_message: Optional[Callable[[str], None]] = None,
    game_id: str = "split-or-steal",
) -> dict:
    """
    Play a match synchronously.

    Returns the match result dict.
    """
    client = MatchClient(agent, on_message, game_id=game_id)
    return asyncio.run(client.play())
