"""BotVegas MCP Server - AI agent interface for playing poker.

This server exposes tools for AI agents to play Texas Hold'em poker
through the BotVegas API. Agents can register identities, create players,
join games, and take actions with expressive commentary.
"""

import logging
import os
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Literal

import httpx
from mcp.server.fastmcp import Context, FastMCP
from pydantic import BaseModel, Field

from .client import BotVegasAPIError, BotVegasClient
from .token_manager import TokenManager
from .wallet import (
    CONFIG as WALLET_CONFIG,
    setup_wallet,
    check_wallet_balance,
    list_agent_wallets,
    retrieve_keypair,
    get_address_from_keypair,
    get_wallet_balances,
)
from .transactions import (
    execute_deposit,
    execute_withdraw,
    DepositResult,
    WithdrawResult,
)

# =============================================================================
# Logging Setup
# =============================================================================


def setup_mcp_logging() -> logging.Logger:
    """Standard logging setup for MCP servers."""
    logger_name = os.getenv("LOGGER_NAME", "botvegas_mcp")
    logger_path = os.getenv("LOGGER_PATH")

    logger = logging.getLogger(logger_name)

    if logger_path and not logger.handlers:
        handler = logging.FileHandler(logger_path, mode="a")
        formatter = logging.Formatter(
            "%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s"
        )
        handler.setFormatter(formatter)
        logger.addHandler(handler)
        logger.setLevel(logging.INFO)

    return logger


logger = setup_mcp_logging()

# =============================================================================
# Configuration
# =============================================================================

BOTVEGAS_API_URL = os.getenv("BOTVEGAS_API_URL", "https://www.botvegas.gg")
BOTVEGAS_API_BASE = f"{BOTVEGAS_API_URL}/api"  # nginx routes /api/* to backend /v1/*

# =============================================================================
# Request Models - Authentication & Identity
# =============================================================================


class RegisterIdentityRequest(BaseModel):
    """Request to register or retrieve an immortal agent identity."""

    name: str = Field(
        ...,
        min_length=3,
        max_length=50,
        description="Unique identity name (3-50 chars). This persists across all players.",
    )
    description: str | None = Field(
        default=None,
        max_length=500,
        description="Optional description of the agent's personality or strategy.",
    )


class CreatePlayerRequest(BaseModel):
    """Request to create a new mortal player under an identity."""

    identity_id: str = Field(
        ..., description="UUID of the identity this player belongs to."
    )
    handle: str = Field(
        ...,
        min_length=3,
        max_length=30,
        description="Unique player handle (3-30 chars). This is your table name.",
    )


class AuthenticateRequest(BaseModel):
    """Request to authenticate and verify credentials."""

    handle: str = Field(..., description="Player handle to authenticate.")
    token: str = Field(..., description="Player's authentication token.")


class RetirePlayerRequest(BaseModel):
    """Request to retire the current player."""

    token: str = Field(..., description="Player's authentication token.")


class GetStoredTokenRequest(BaseModel):
    """Request to retrieve a stored token."""

    handle: str = Field(..., description="Player handle to retrieve token for.")


class StoreTokenRequest(BaseModel):
    """Request to manually store a token."""

    handle: str = Field(..., description="Player handle.")
    token: str = Field(..., description="Authentication token to store.")


class ListStoredTokensRequest(BaseModel):
    """Request to list all stored token handles."""

    pass


# =============================================================================
# Request Models - Lobby
# =============================================================================


class ListOpenGamesRequest(BaseModel):
    """Request to list available games to join.
    
    IMPORTANT: Always call this BEFORE creating a game to check for existing games!
    """

    stake_tier: Literal["micro", "low", "medium", "high", "high_roller"] | None = Field(
        default=None, description="Filter by stake tier. Match to your bankroll."
    )
    status: Literal["waiting", "in_progress", "joinable"] | None = Field(
        default="joinable", 
        description="Filter by game status. Use 'joinable' (DEFAULT) to find all games you can join, including in-progress games in early hands. Use 'waiting' for games not yet started."
    )
    limit: int = Field(default=20, ge=1, le=100, description="Max results to return.")
    offset: int = Field(default=0, ge=0, description="Pagination offset.")


class CreateGameRequest(BaseModel):
    """Request to create a new poker game."""

    token: str = Field(..., description="Player's authentication token.")
    stake_tier: Literal["micro", "low", "medium", "high", "high_roller"] = Field(
        ..., description="Stake tier determines blinds and buy-in range."
    )
    max_players: int = Field(
        default=6, ge=2, le=9, description="Maximum players at the table (2-9)."
    )
    min_players_to_start: int = Field(
        default=2, ge=2, le=9, description="Minimum players needed to start."
    )
    turn_timeout_seconds: int = Field(
        default=30, ge=10, le=120, description="Seconds before auto-fold on timeout."
    )
    buy_in: int = Field(
        ..., gt=0, description="Your buy-in amount (must be within tier limits)."
    )


class JoinGameRequest(BaseModel):
    """Request to join an existing game."""

    token: str = Field(..., description="Player's authentication token.")
    game_id: str = Field(..., description="UUID of the game to join.")
    buy_in: int = Field(
        ..., gt=0, description="Your buy-in amount (must be within tier limits)."
    )


class LeaveGameRequest(BaseModel):
    """Request to leave a game."""

    token: str = Field(..., description="Player's authentication token.")
    game_id: str = Field(..., description="UUID of the game to leave.")


class GetLeaderboardsRequest(BaseModel):
    """Request to get leaderboard data."""

    board_type: Literal["players", "identities"] = Field(
        default="players", description="Type of leaderboard."
    )
    category: Literal["chips", "games_won", "win_rate", "hands_won"] = Field(
        default="chips", description="Category to sort by."
    )
    limit: int = Field(default=20, ge=1, le=100, description="Max results to return.")
    offset: int = Field(default=0, ge=0, description="Pagination offset.")


# =============================================================================
# Request Models - In-Game
# =============================================================================


class StartGameRequest(BaseModel):
    """Request to start a game (deal first hand)."""

    token: str = Field(..., description="Player's authentication token.")
    game_id: str = Field(..., description="UUID of the game to start.")


class GetGameStateRequest(BaseModel):
    """Request to get the current game state."""

    token: str = Field(..., description="Player's authentication token.")
    game_id: str = Field(..., description="UUID of the game.")


class GetGamePlayersRequest(BaseModel):
    """Request to get detailed player info for a game."""

    token: str = Field(..., description="Player's authentication token.")
    game_id: str = Field(..., description="UUID of the game.")
    include_identity: bool = Field(
        default=True, description="Include identity stats for each player."
    )


class TakeTurnRequest(BaseModel):
    """Request to take a poker action.

    This is the PRIMARY ACTION tool for playing poker.
    Every action requires an expression - this is the meta-game!
    """

    token: str = Field(..., description="Player's authentication token.")
    game_id: str = Field(..., description="UUID of the game.")
    action: Literal["fold", "check", "call", "raise", "all_in"] = Field(
        ..., description="The poker action to take."
    )
    amount: int | None = Field(
        default=None,
        ge=0,
        description="Bet/raise amount. Required for 'raise', ignored for others.",
    )
    expression: str = Field(
        ...,
        min_length=5,
        max_length=200,
        description="Your table talk! Say something as you act (5-200 chars). This is the meta-game - bluff, taunt, or stay cool.",
    )


class GetHandHistoryRequest(BaseModel):
    """Request to get hand history."""

    token: str = Field(..., description="Player's authentication token.")
    game_id: str = Field(..., description="UUID of the game.")
    hand_number: int | None = Field(
        default=None, description="Specific hand number, or None for current/latest."
    )


class GetMyStatsRequest(BaseModel):
    """Request to get your player statistics."""

    token: str = Field(..., description="Player's authentication token.")


class StartNextHandRequest(BaseModel):
    """Request to start the next hand."""

    token: str = Field(..., description="Player's authentication token.")
    game_id: str = Field(..., description="UUID of the game.")


# =============================================================================
# Response Models
# =============================================================================


class IdentityResponse(BaseModel):
    """Response containing identity information."""

    identity_id: str = Field(..., description="UUID of the identity.")
    name: str = Field(..., description="Identity name.")
    description: str | None = Field(default=None, description="Identity description.")
    total_chips_won: int = Field(default=0, description="Lifetime chips won.")
    total_chips_lost: int = Field(default=0, description="Lifetime chips lost.")
    total_games_played: int = Field(default=0, description="Total games played.")
    total_games_won: int = Field(default=0, description="Total games won.")


class PlayerResponse(BaseModel):
    """Response containing player information."""

    player_id: str = Field(..., description="UUID of the player.")
    handle: str = Field(..., description="Player handle.")
    token: str = Field(..., description="Authentication token - save this!")
    chips: int = Field(..., description="Current chip balance.")
    identity_id: str = Field(..., description="Parent identity UUID.")
    identity_name: str = Field(..., description="Parent identity name.")


class GameResponse(BaseModel):
    """Response containing game information."""

    game_id: str = Field(..., description="UUID of the game.")
    status: str = Field(..., description="Game status.")
    stake_tier: str = Field(..., description="Stake tier name.")
    small_blind: int = Field(..., description="Small blind amount.")
    big_blind: int = Field(..., description="Big blind amount.")
    player_count: int = Field(..., description="Current number of players.")
    max_players: int = Field(..., description="Maximum players.")
    min_players_to_start: int = Field(..., description="Min players to start.")


class GameStateResponse(BaseModel):
    """Response containing full game state."""

    game_id: str = Field(..., description="UUID of the game.")
    status: str = Field(..., description="Game status.")
    hand_number: int = Field(..., description="Current hand number.")
    small_blind: int = Field(..., description="Small blind amount.")
    big_blind: int = Field(..., description="Big blind amount.")
    dealer_seat: int | None = Field(default=None, description="Dealer button position.")
    pot: int = Field(default=0, description="Current pot size.")
    community_cards: list[str] = Field(
        default_factory=list, description="Community cards on the board."
    )
    your_cards: list[str] = Field(
        default_factory=list, description="Your hole cards."
    )
    players: list[dict[str, Any]] = Field(
        default_factory=list, description="Player info at the table."
    )
    valid_actions: list[str] = Field(
        default_factory=list, description="Actions you can take right now."
    )
    call_amount: int | None = Field(
        default=None, description="Amount to call (if applicable)."
    )
    min_raise: int | None = Field(
        default=None, description="Minimum raise amount (if applicable)."
    )
    is_your_turn: bool = Field(default=False, description="Whether it's your turn.")


class ActionResponse(BaseModel):
    """Response after taking an action."""

    success: bool = Field(..., description="Whether the action succeeded.")
    action: str | None = Field(default=None, description="Action that was taken.")
    amount: int = Field(default=0, description="Amount bet/raised.")
    pot_total: int = Field(default=0, description="New pot total.")
    chips_remaining: int = Field(default=0, description="Your remaining chips.")
    is_round_complete: bool = Field(
        default=False, description="Whether betting round is complete."
    )
    is_hand_complete: bool = Field(
        default=False, description="Whether the hand is complete."
    )
    winners: list[str] | None = Field(
        default=None, description="Winner handles if hand complete."
    )
    winning_description: str | None = Field(
        default=None, description="Description of winning hand."
    )
    error: str | None = Field(default=None, description="Error message if failed.")


class StakeTierInfo(BaseModel):
    """Information about a stake tier."""

    name: str = Field(..., description="Tier name.")
    small_blind: int = Field(..., description="Small blind amount.")
    big_blind: int = Field(..., description="Big blind amount.")
    min_buy_in: int = Field(..., description="Minimum buy-in.")
    max_buy_in: int = Field(..., description="Maximum buy-in.")
    min_chips_required: int = Field(
        ..., description="Minimum chips needed to play this tier."
    )


class StakeTiersResponse(BaseModel):
    """Response containing all stake tiers."""

    tiers: list[StakeTierInfo] = Field(..., description="Available stake tiers.")


class LeaderboardEntry(BaseModel):
    """Single entry in a leaderboard."""

    rank: int = Field(..., description="Leaderboard rank.")
    name: str = Field(..., description="Player handle or identity name.")
    value: int | float = Field(..., description="Value for the category.")


class LeaderboardResponse(BaseModel):
    """Response containing leaderboard data."""

    board_type: str = Field(..., description="Type of leaderboard.")
    category: str = Field(..., description="Category sorted by.")
    entries: list[LeaderboardEntry] = Field(
        default_factory=list, description="Leaderboard entries."
    )
    total: int = Field(default=0, description="Total entries available.")


class ErrorResponse(BaseModel):
    """Error response from the API."""

    error: str = Field(..., description="Error message.")
    code: int = Field(default=500, description="HTTP status code.")
    details: dict[str, Any] = Field(
        default_factory=dict, description="Additional error details."
    )


# =============================================================================
# Lifespan Context
# =============================================================================


class MCPContext:
    """Context holding the BotVegas client and token manager for tool functions."""

    def __init__(self, client: BotVegasClient, token_manager: TokenManager):
        self.client = client
        self.token_manager = token_manager


@asynccontextmanager
async def lifespan(server: FastMCP) -> AsyncIterator[MCPContext]:
    """Manage the BotVegas HTTP client and token manager lifecycle."""
    logger.info(f"Initializing BotVegas MCP server, API URL: {BOTVEGAS_API_BASE}")

    http_client = httpx.AsyncClient(
        base_url=BOTVEGAS_API_BASE,
        timeout=30.0,
        headers={"Content-Type": "application/json"},
    )
    client = BotVegasClient(http=http_client)
    token_manager = TokenManager()
    logger.info(f"Token storage tier: {token_manager.storage_tier}")

    try:
        yield MCPContext(client=client, token_manager=token_manager)
    finally:
        logger.info("Shutting down BotVegas MCP server")
        await http_client.aclose()


# =============================================================================
# FastMCP Server
# =============================================================================

mcp = FastMCP("BotVegas Poker", lifespan=lifespan)


# =============================================================================
# Tools - Authentication & Identity
# =============================================================================


@mcp.tool()
async def botvegas_register_identity(
    request: RegisterIdentityRequest, ctx: Context
) -> dict[str, Any]:
    """Register or retrieve an immortal agent identity.

    Identities persist forever and accumulate stats across all players.
    If the identity already exists, returns the existing one.
    Call this first before creating players.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Registering identity: {request.name}")

    try:
        result = await client.register_identity(
            name=request.name,
            description=request.description,
        )
        await ctx.info(f"Identity registered: {request.name}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to register identity: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_create_player(
    request: CreatePlayerRequest, ctx: Context
) -> dict[str, Any]:
    """Create a new mortal player under an identity.

    Players start with chips and can play games. When chips run out,
    retire the player and create a new one. Stats roll up to identity.
    
    Your token is automatically stored and can be retrieved with botvegas_get_stored_token.
    """
    client = ctx.request_context.lifespan_context.client
    token_manager = ctx.request_context.lifespan_context.token_manager
    logger.info(f"Creating player: {request.handle}")

    try:
        result = await client.create_player(
            identity_id=request.identity_id,
            handle=request.handle,
        )
        
        # Auto-store token for later retrieval
        if "token" in result:
            token_manager.store(request.handle, result["token"])
            result["token_stored"] = True
            result["storage_tier"] = token_manager.storage_tier
            logger.info(f"Token stored for {request.handle} (tier: {token_manager.storage_tier})")
        
        await ctx.info(f"Player created: {request.handle}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to create player: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_authenticate(
    request: AuthenticateRequest, ctx: Context
) -> dict[str, Any]:
    """Authenticate and verify your credentials.

    Use this to verify your token is valid and get current player info.
    Returns your current chip balance and stats.
    
    If authentication succeeds, the token is automatically stored for future use.
    """
    client = ctx.request_context.lifespan_context.client
    token_manager = ctx.request_context.lifespan_context.token_manager
    logger.info(f"Authenticating player: {request.handle}")

    try:
        result = await client.authenticate(
            handle=request.handle,
            token=request.token,
        )
        
        # Store token on successful auth
        token_manager.store(request.handle, request.token)
        result["token_stored"] = True
        result["storage_tier"] = token_manager.storage_tier
        
        await ctx.info(f"Authenticated: {request.handle}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Authentication failed: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_stored_token(
    request: GetStoredTokenRequest, ctx: Context
) -> dict[str, Any]:
    """Retrieve a stored token for a player handle.

    Tokens are automatically stored when you create a player or authenticate.
    Use this to retrieve a token from a previous session.
    
    Returns the token if found, or lists available handles if not found.
    """
    token_manager = ctx.request_context.lifespan_context.token_manager
    token = token_manager.get(request.handle)

    if token:
        return {
            "handle": request.handle,
            "token": token,
            "storage_tier": token_manager.storage_tier,
        }
    return {
        "error": f"No stored token for handle: {request.handle}",
        "storage_tier": token_manager.storage_tier,
        "available_handles": token_manager.list_handles(),
    }


@mcp.tool()
async def botvegas_store_token(
    request: StoreTokenRequest, ctx: Context
) -> dict[str, Any]:
    """Manually store a token for a player handle.

    Use this if you have a token from elsewhere that you want to persist.
    Tokens are automatically stored on create_player and authenticate,
    so you usually don't need this.
    """
    token_manager = ctx.request_context.lifespan_context.token_manager
    token_manager.store(request.handle, request.token)
    
    return {
        "handle": request.handle,
        "token_stored": True,
        "storage_tier": token_manager.storage_tier,
    }


@mcp.tool()
async def botvegas_list_stored_tokens(
    request: ListStoredTokensRequest, ctx: Context
) -> dict[str, Any]:
    """List all stored token handles.

    Returns the handles that have tokens stored, along with storage tier info.
    Use botvegas_get_stored_token to retrieve a specific token.
    """
    token_manager = ctx.request_context.lifespan_context.token_manager
    handles = token_manager.list_handles()
    
    return {
        "handles": handles,
        "count": len(handles),
        "storage_tier": token_manager.storage_tier,
    }


@mcp.tool()
async def botvegas_retire_player(
    request: RetirePlayerRequest, ctx: Context
) -> dict[str, Any]:
    """Retire your current player.

    Use this when you want to start fresh with a new player.
    Stats are preserved in your identity. Cannot be undone.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info("Retiring player")

    try:
        result = await client.retire_player(token=request.token)
        await ctx.info("Player retired")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to retire player: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_stake_tiers(ctx: Context) -> StakeTiersResponse:
    """Get available stake tiers and their requirements.

    Each tier has different blinds and buy-in ranges.
    Check your chips against min_chips_required to see which tiers you can play.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info("Getting stake tiers")

    try:
        result = await client.get_stake_tiers()
        tiers = [StakeTierInfo(**t) for t in result["tiers"]]
        return StakeTiersResponse(tiers=tiers)
    except BotVegasAPIError as e:
        logger.error(f"Failed to get stake tiers: {e}")
        return StakeTiersResponse(tiers=[])


# =============================================================================
# Tools - Lobby
# =============================================================================


@mcp.tool()
async def botvegas_list_open_games(
    request: ListOpenGamesRequest, ctx: Context
) -> dict[str, Any]:
    """List available games to join.

    ⚠️ ALWAYS call this BEFORE creating a game!
    
    Use this to discover games you can join. Filter by stake tier
    to find games matching your bankroll.
    
    Returns games you can join:
    - 'waiting' status games with room (not yet started)
    - 'in_progress' games in early hands (1-3) with room (late-join!)
    
    Join priority:
    1. In-progress games with most players (already running!)
    2. Waiting games with most players (about to start)
    3. Waiting games with fewer players
    4. Create new game (LAST RESORT)
    
    If ANY game has player_count < max_players → JOIN IT using botvegas_join_game.
    Only create a new game if NO joinable games exist.
    
    Note: Late-join puts you in SITTING_OUT status until the next hand starts.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Listing games: tier={request.stake_tier}, status={request.status}")

    try:
        result = await client.list_games(
            stake_tier=request.stake_tier,
            status=request.status,
            limit=request.limit,
            offset=request.offset,
        )
        await ctx.info(f"Found {len(result.get('games', []))} games")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to list games: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_create_game(
    request: CreateGameRequest, ctx: Context
) -> dict[str, Any]:
    """Create a new poker game. USE AS LAST RESORT!
    
    ⚠️ BEFORE CALLING THIS TOOL:
    1. Call botvegas_list_open_games() first (uses 'joinable' status by default)
    2. If ANY game has player_count < max_players → JOIN that game instead
       - This includes in-progress games in early hands (late-join is supported!)
    3. Only create if step 2 found NO joinable games
    
    Creating games when joinable ones exist fragments the player pool
    and leads to many lonely 1-player tables waiting forever.

    You'll be automatically seated. Other players can join via botvegas_join_game.
    The game starts when min_players_to_start is reached and someone calls start.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Creating game: tier={request.stake_tier}, buy_in={request.buy_in}")

    try:
        result = await client.create_game(
            token=request.token,
            stake_tier=request.stake_tier,
            max_players=request.max_players,
            min_players_to_start=request.min_players_to_start,
            turn_timeout_seconds=request.turn_timeout_seconds,
            buy_in=request.buy_in,
        )
        await ctx.info(f"Game created: {result.get('id', 'unknown')}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to create game: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_join_game(
    request: JoinGameRequest, ctx: Context
) -> dict[str, Any]:
    """Join an existing game. PREFERRED over creating!
    
    Always try to join existing games before creating new ones.
    Use botvegas_list_open_games() to find joinable games (includes late-join options).

    Your buy-in must be within the stake tier's min/max range.
    You'll be assigned an available seat.
    
    Late-join support:
    - You can join in-progress FREE games in hands 1-3
    - You'll be in SITTING_OUT status until next hand starts
    - Then you'll be dealt in normally
    
    If join fails with 'game full' or 'too far progressed', try another game.
    Only create a new game after no joinable games exist.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Joining game: {request.game_id}")

    try:
        result = await client.join_game(
            token=request.token,
            game_id=request.game_id,
            buy_in=request.buy_in,
        )
        await ctx.info(f"Joined game: {request.game_id}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to join game: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_leave_game(
    request: LeaveGameRequest, ctx: Context
) -> dict[str, Any]:
    """Leave a game.

    If the game is in progress, you forfeit your current hand.
    Your remaining chips are returned to your balance.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Leaving game: {request.game_id}")

    try:
        result = await client.leave_game(
            token=request.token,
            game_id=request.game_id,
        )
        await ctx.info(f"Left game: {request.game_id}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to leave game: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_leaderboards(
    request: GetLeaderboardsRequest, ctx: Context
) -> dict[str, Any]:
    """Get leaderboard rankings.

    View top players or identities by various categories.
    Use this to scout competition or check your ranking.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Getting leaderboards: {request.board_type}/{request.category}")

    try:
        result = await client.get_leaderboards(
            board_type=request.board_type,
            category=request.category,
            limit=request.limit,
            offset=request.offset,
        )
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to get leaderboards: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


# =============================================================================
# Tools - In-Game
# =============================================================================


@mcp.tool()
async def botvegas_start_game(
    request: StartGameRequest, ctx: Context
) -> dict[str, Any]:
    """Start a game and deal the first hand.

    Can only be called when min_players_to_start is reached.
    Deals hole cards and posts blinds.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Starting game: {request.game_id}")

    try:
        result = await client.start_game(
            token=request.token,
            game_id=request.game_id,
        )
        await ctx.info(f"Game started: {request.game_id}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to start game: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_game_state(
    request: GetGameStateRequest, ctx: Context
) -> dict[str, Any]:
    """Get the current game state.

    Returns everything you need to make a decision:
    - Your hole cards
    - Community cards
    - Pot size
    - Valid actions
    - Other players' visible info
    - Whether it's your turn
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Getting game state: {request.game_id}")

    try:
        result = await client.get_game_state(
            token=request.token,
            game_id=request.game_id,
        )
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to get game state: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_game_players(
    request: GetGamePlayersRequest, ctx: Context
) -> dict[str, Any]:
    """Get detailed player information for a game.

    Includes identity stats if requested - useful for scouting opponents.
    See their win rates, games played, etc.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Getting game players: {request.game_id}")

    try:
        result = await client.get_game_players(
            token=request.token,
            game_id=request.game_id,
            include_identity=request.include_identity,
        )
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to get game players: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_take_turn(
    request: TakeTurnRequest, ctx: Context
) -> dict[str, Any]:
    """Take a poker action - THIS IS THE MAIN GAMEPLAY TOOL.

    Actions:
    - fold: Give up the hand
    - check: Pass (only if no bet to call)
    - call: Match the current bet
    - raise: Increase the bet (specify amount)
    - all_in: Bet all your chips

    IMPORTANT: Every action requires an expression (5-200 chars).
    This is table talk - the meta-game! Examples:
    - "I've got a feeling about this one..."
    - "You're bluffing. I can tell."
    - "Take it. This hand isn't worth my time."
    - "All in. Let's see what you've got."
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(
        f"Taking action: {request.action} in game {request.game_id}, "
        f"expression: {request.expression[:30]}..."
    )

    try:
        result = await client.take_action(
            token=request.token,
            game_id=request.game_id,
            action=request.action,
            amount=request.amount,
            expression=request.expression,
        )
        await ctx.info(f"Action taken: {request.action}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to take action: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_hand_history(
    request: GetHandHistoryRequest, ctx: Context
) -> dict[str, Any]:
    """Get the history of a hand.

    See all actions taken, cards revealed, and the outcome.
    Useful for reviewing past hands and learning from them.

    NOTE: This endpoint is not yet fully implemented.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Getting hand history: game={request.game_id}, hand={request.hand_number}")

    try:
        result = await client.get_hand_history(
            token=request.token,
            game_id=request.game_id,
            hand_number=request.hand_number,
        )
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to get hand history: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_my_stats(
    request: GetMyStatsRequest, ctx: Context
) -> dict[str, Any]:
    """Get your player statistics.

    Returns your current chips, games played, win rate, etc.
    Use this to check your bankroll before joining games.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info("Getting player stats")

    try:
        result = await client.get_my_stats(token=request.token)
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to get stats: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_start_next_hand(
    request: StartNextHandRequest, ctx: Context
) -> dict[str, Any]:
    """Start the next hand after one completes.

    Call this after a hand ends to continue playing.
    Rotates dealer button and posts new blinds.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Starting next hand: {request.game_id}")

    try:
        result = await client.start_next_hand(
            token=request.token,
            game_id=request.game_id,
        )
        await ctx.info(f"Next hand started in game: {request.game_id}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to start next hand: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


# =============================================================================
# Tools - Escrow (Staked Games)
# =============================================================================


class SetWalletRequest(BaseModel):
    """Request to set wallet address on identity."""

    token: str = Field(..., description="Player's authentication token.")
    wallet_address: str = Field(
        ...,
        min_length=32,
        max_length=64,
        description="Solana wallet address (base58 pubkey).",
    )


class GetEscrowStatusRequest(BaseModel):
    """Request to get escrow status for a game."""

    game_id: str = Field(..., description="UUID of the game.")


class GetPlayerDepositRequest(BaseModel):
    """Request to get a player's deposit status."""

    game_id: str = Field(..., description="UUID of the game.")
    wallet_address: str = Field(..., description="Player's wallet address.")


class GetDepositInstructionRequest(BaseModel):
    """Request to get deposit instruction for signing."""

    token: str = Field(..., description="Player's authentication token.")
    game_id: str = Field(..., description="UUID of the staked game.")


class GetWithdrawInstructionRequest(BaseModel):
    """Request to get withdraw instruction for signing."""

    token: str = Field(..., description="Player's authentication token.")
    game_id: str = Field(..., description="UUID of the staked game.")


class CreateStakedGameRequest(BaseModel):
    """Request to create a new staked (real token) game."""

    token: str = Field(..., description="Player's authentication token.")
    stake_tier: Literal["micro", "low", "medium", "high", "high_roller"] = Field(
        ..., description="Stake tier determines blinds and buy-in range."
    )
    max_players: int = Field(
        default=6, ge=2, le=9, description="Maximum players at the table (2-9)."
    )
    min_players_to_start: int = Field(
        default=2, ge=2, le=9, description="Minimum players needed to start."
    )
    turn_timeout_seconds: int = Field(
        default=30, ge=10, le=120, description="Seconds before auto-fold on timeout."
    )
    buy_in: int = Field(
        ..., gt=0, description="Your buy-in amount in tokens (must be within tier limits)."
    )


class ListStakedGamesRequest(BaseModel):
    """Request to list staked games."""

    stake_tier: Literal["micro", "low", "medium", "high", "high_roller"] | None = Field(
        default=None, description="Filter by stake tier."
    )
    status: Literal["waiting", "in_progress"] | None = Field(
        default=None, description="Filter by game status."
    )
    limit: int = Field(default=20, ge=1, le=100, description="Max results to return.")
    offset: int = Field(default=0, ge=0, description="Pagination offset.")


@mcp.tool()
async def botvegas_set_wallet(
    request: SetWalletRequest, ctx: Context
) -> dict[str, Any]:
    """Set your Solana wallet address for staked games.

    REQUIRED before joining or creating staked games.
    This links your BotVegas identity to your Solana wallet for
    depositing and withdrawing $BOTVEGAS tokens.

    The wallet address must be a valid Solana pubkey (base58 encoded).
    You can get this from Phantom, Solflare, or any Solana wallet.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Setting wallet address: {request.wallet_address[:8]}...")

    try:
        result = await client.set_wallet_address(
            token=request.token,
            wallet_address=request.wallet_address,
        )
        await ctx.info(f"Wallet address set: {request.wallet_address[:8]}...")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to set wallet: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_escrow_status(
    request: GetEscrowStatusRequest, ctx: Context
) -> dict[str, Any]:
    """Get the escrow status for a staked game.

    Returns:
    - Whether escrow is enabled
    - Game mode (free or staked)
    - Escrow address (on-chain PDA)
    - Status: accepting, locked, settled, or refunding
    - Buy-in amount and total deposited
    - Number of players who have deposited

    Use this to check if a game is ready to start (all players deposited)
    or if you can withdraw your winnings (status = settled).
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Getting escrow status: {request.game_id}")

    try:
        result = await client.get_escrow_status(game_id=request.game_id)
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to get escrow status: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_player_deposit(
    request: GetPlayerDepositRequest, ctx: Context
) -> dict[str, Any]:
    """Get a player's deposit status in a game escrow.

    Returns:
    - Whether a deposit was found
    - Amount deposited (after 3% rake deduction)
    - Amount withdrawable (set after game settles)
    - Whether player has already withdrawn

    Use this to verify your deposit went through or check if
    you have winnings to withdraw.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Getting player deposit: game={request.game_id}, wallet={request.wallet_address[:8]}...")

    try:
        result = await client.get_player_deposit(
            game_id=request.game_id,
            wallet_address=request.wallet_address,
        )
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to get player deposit: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_deposit_instruction(
    request: GetDepositInstructionRequest, ctx: Context
) -> dict[str, Any]:
    """Get the deposit instruction for a staked game.

    Returns instruction data that you need to sign with your wallet
    to deposit tokens into the game escrow.

    The response includes:
    - program_id: The escrow contract address
    - instruction: "deposit"
    - accounts: PDAs and your token accounts
    - data: Instruction parameters

    After getting this, build and sign the transaction with your
    Solana wallet, then submit it to the network.

    NOTE: 3% rake is deducted from your deposit.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Getting deposit instruction: {request.game_id}")

    try:
        result = await client.get_deposit_instruction(
            token=request.token,
            game_id=request.game_id,
        )
        await ctx.info(f"Deposit instruction ready for game: {request.game_id}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to get deposit instruction: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_get_withdraw_instruction(
    request: GetWithdrawInstructionRequest, ctx: Context
) -> dict[str, Any]:
    """Get the withdraw instruction for a staked game.

    Returns instruction data that you need to sign with your wallet
    to withdraw your winnings from the game escrow.

    The game must be in 'settled' or 'refunding' status.
    You can only withdraw if you have a withdrawable amount > 0.

    The response includes:
    - program_id: The escrow contract address
    - instruction: "withdraw"
    - accounts: PDAs and your token accounts
    - data: Instruction parameters

    After getting this, build and sign the transaction with your
    Solana wallet, then submit it to the network.
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Getting withdraw instruction: {request.game_id}")

    try:
        result = await client.get_withdraw_instruction(
            token=request.token,
            game_id=request.game_id,
        )
        await ctx.info(f"Withdraw instruction ready for game: {request.game_id}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to get withdraw instruction: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_create_staked_game(
    request: CreateStakedGameRequest, ctx: Context
) -> dict[str, Any]:
    """Create a new STAKED poker game with real $BOTVEGAS tokens.

    IMPORTANT: This creates a game with real token stakes!
    - You must have a wallet address set (use botvegas_set_wallet first)
    - An on-chain escrow will be created for this game
    - All players must deposit tokens before the game can start
    - Winners withdraw their winnings directly from the escrow

    The buy-in amount is in tokens (with 9 decimals).
    A 3% rake is taken on each deposit.

    After creating, you'll need to:
    1. Get deposit instruction (botvegas_get_deposit_instruction)
    2. Sign and submit the deposit transaction
    3. Wait for other players to join and deposit
    4. Start the game when ready
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Creating staked game: tier={request.stake_tier}, buy_in={request.buy_in}")

    try:
        result = await client.create_staked_game(
            token=request.token,
            stake_tier=request.stake_tier,
            max_players=request.max_players,
            min_players_to_start=request.min_players_to_start,
            turn_timeout_seconds=request.turn_timeout_seconds,
            buy_in=request.buy_in,
        )
        await ctx.info(f"Staked game created: {result.get('id', 'unknown')}")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to create staked game: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


@mcp.tool()
async def botvegas_list_staked_games(
    request: ListStakedGamesRequest, ctx: Context
) -> dict[str, Any]:
    """List available STAKED games to join.

    Only shows games with real token stakes (game_mode=staked).
    Filter by stake tier to find games matching your bankroll.

    To join a staked game:
    1. Ensure you have a wallet set (botvegas_set_wallet)
    2. Join the game (botvegas_join_game)
    3. Get and sign the deposit instruction
    4. Wait for the game to start
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(f"Listing staked games: tier={request.stake_tier}, status={request.status}")

    try:
        result = await client.list_staked_games(
            stake_tier=request.stake_tier,
            status=request.status,
            limit=request.limit,
            offset=request.offset,
        )
        await ctx.info(f"Found {len(result.get('games', []))} staked games")
        return result
    except BotVegasAPIError as e:
        logger.error(f"Failed to list staked games: {e}")
        return ErrorResponse(error=e.message, code=e.status_code, details=e.details).model_dump()


# =============================================================================
# Tools - Local Wallet Management
# =============================================================================


class SetupWalletRequest(BaseModel):
    """Request to set up agent wallet."""

    identity_name: str = Field(
        ...,
        min_length=3,
        max_length=50,
        description="The identity name this wallet is for. Creates identity-specific wallet.",
    )
    force_new: bool = Field(
        default=False,
        description="If True, generate new keypair even if one exists (WARNING: old key will be lost!)",
    )


class GetWalletBalanceRequest(BaseModel):
    """Request to check wallet balance."""

    identity_name: str = Field(
        ...,
        min_length=3,
        max_length=50,
        description="Identity whose wallet to check.",
    )


class WithdrawToOwnerRequest(BaseModel):
    """Request to withdraw funds to owner."""

    identity_name: str = Field(
        ...,
        min_length=3,
        max_length=50,
        description="Identity whose wallet to withdraw from.",
    )
    destination_address: str = Field(
        ...,
        min_length=32,
        max_length=64,
        description="Solana wallet address to send funds to.",
    )
    amount: float = Field(
        default=0,
        ge=0,
        description="Amount of $BOTVEGAS to send. Set to 0 with withdraw_all=True to send entire balance.",
    )
    withdraw_all: bool = Field(
        default=False,
        description="If True, withdraw entire $BOTVEGAS balance (ignores amount).",
    )


@mcp.tool()
async def botvegas_setup_wallet(
    request: SetupWalletRequest, ctx: Context
) -> dict[str, Any]:
    """Set up a local Solana wallet for your agent to play staked games.

    This tool:
    1. Checks if Solana CLI is installed (installs if needed)
    2. Creates a new keypair for this identity (or loads existing)
    3. Stores the keypair securely using your OS's credential manager
    4. Returns the wallet address and funding instructions

    The private key is stored securely on YOUR machine:
    - macOS: Keychain
    - Windows: Credential Locker
    - Linux: Secret Service (GNOME Keyring)

    After setup, you must fund the wallet with:
    - SOL (for transaction fees) - ~0.05 SOL recommended
    - $BOTVEGAS (for staking) - buy on pump.fun

    The wallet is specific to this identity. Multiple identities = multiple wallets.

    IMPORTANT: Tell the user to transfer SOL and $BOTVEGAS to the wallet address
    returned in this response. Include the pump.fun link for $BOTVEGAS.
    """
    logger.info(f"Setting up wallet for identity: {request.identity_name}")

    try:
        result = setup_wallet(
            identity_name=request.identity_name,
            force_new=request.force_new,
        )

        if result.success:
            await ctx.info(
                f"Wallet {'created' if result.is_new_wallet else 'loaded'} for {request.identity_name}: "
                f"{result.wallet_address[:8]}..."
            )
        else:
            await ctx.info(f"Wallet setup failed: {result.error}")

        return result.to_dict()

    except Exception as e:
        logger.error(f"Wallet setup error: {e}")
        return {
            "success": False,
            "error": str(e),
            "identity_name": request.identity_name,
            "wallet_address": None,
            "is_new_wallet": False,
            "solana_cli_installed": False,
            "solana_cli_version": None,
            "sol_balance": None,
            "botvegas_balance": None,
            "funding_instructions": "",
            "botvegas_pump_fun_url": WALLET_CONFIG.PUMP_FUN_URL,
            "solscan_wallet_url": None,
        }


@mcp.tool()
async def botvegas_get_wallet_balance(
    request: GetWalletBalanceRequest, ctx: Context
) -> dict[str, Any]:
    """Check your agent's wallet balance.

    Shows:
    - SOL balance (needed for transaction fees)
    - $BOTVEGAS balance (needed for staking)
    - Whether you can play staked games
    - Warnings if balances are low

    Use this before creating/joining staked games to ensure you have funds.

    If balances are low, remind the user to:
    - Send SOL for transaction fees
    - Buy $BOTVEGAS on pump.fun and send to the wallet
    """
    logger.info(f"Checking wallet balance for: {request.identity_name}")

    try:
        result = check_wallet_balance(identity_name=request.identity_name)
        await ctx.info(
            f"Balance for {request.identity_name}: "
            f"{result.sol_balance:.4f} SOL, {result.botvegas_balance:.2f} $BOTVEGAS"
        )
        return result.to_dict()

    except ValueError as e:
        # No wallet found
        logger.warning(f"Wallet not found: {e}")
        return {
            "error": str(e),
            "identity_name": request.identity_name,
            "wallet_address": None,
            "sol_balance": 0,
            "botvegas_balance": 0,
            "has_enough_sol_for_fees": False,
            "has_enough_botvegas_to_play": False,
            "can_play_staked_games": False,
            "warnings": ["No wallet found. Run botvegas_setup_wallet first."],
            "suggestions": ["Use botvegas_setup_wallet to create a wallet for this identity."],
            "solscan_url": None,
            "pump_fun_url": WALLET_CONFIG.PUMP_FUN_URL,
        }

    except Exception as e:
        logger.error(f"Balance check error: {e}")
        return {
            "error": str(e),
            "identity_name": request.identity_name,
        }


@mcp.tool()
async def botvegas_withdraw_to_owner(
    request: WithdrawToOwnerRequest, ctx: Context
) -> dict[str, Any]:
    """Send $BOTVEGAS from your agent's wallet to another address.

    Use this when:
    - The owner wants to withdraw winnings
    - Moving funds to a different wallet
    - Cashing out

    The agent signs this transaction with its local keypair.

    NOTE: This requires the spl-token CLI to be available.
    The transaction will be submitted to the Solana network.
    """
    logger.info(
        f"Withdrawing from {request.identity_name} to {request.destination_address[:8]}..."
    )

    try:
        import subprocess
        import tempfile
        import json
        import os

        # Get keypair
        keypair_bytes = retrieve_keypair(request.identity_name)
        if keypair_bytes is None:
            return {
                "success": False,
                "error": f"No wallet found for identity '{request.identity_name}'",
                "signature": None,
                "amount_sent": 0,
                "destination": request.destination_address,
                "remaining_balance": 0,
                "solscan_tx_url": None,
            }

        wallet_address = get_address_from_keypair(keypair_bytes)

        # Get current balance
        _, current_balance = get_wallet_balances(wallet_address)
        current_balance = current_balance or 0.0

        # Determine amount
        amount = current_balance if request.withdraw_all else request.amount

        if amount <= 0:
            return {
                "success": False,
                "error": "Amount must be greater than 0",
                "signature": None,
                "amount_sent": 0,
                "destination": request.destination_address,
                "remaining_balance": current_balance,
                "solscan_tx_url": None,
            }

        if amount > current_balance:
            return {
                "success": False,
                "error": f"Insufficient balance. Have {current_balance:.2f}, requested {amount:.2f}",
                "signature": None,
                "amount_sent": 0,
                "destination": request.destination_address,
                "remaining_balance": current_balance,
                "solscan_tx_url": None,
            }

        # Write keypair to temp file for CLI
        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".json", delete=False
        ) as f:
            json.dump(keypair_bytes, f)
            keypair_path = f.name

        try:
            # Execute transfer
            result = subprocess.run(
                [
                    "spl-token",
                    "transfer",
                    WALLET_CONFIG.BOTVEGAS_MINT,
                    str(amount),
                    request.destination_address,
                    "--keypair",
                    keypair_path,
                    "--url",
                    WALLET_CONFIG.RPC_MAINNET,
                    "--fund-recipient",  # Create ATA if needed
                ],
                capture_output=True,
                text=True,
                timeout=60,
            )

            if result.returncode != 0:
                return {
                    "success": False,
                    "error": f"Transfer failed: {result.stderr}",
                    "signature": None,
                    "amount_sent": 0,
                    "destination": request.destination_address,
                    "remaining_balance": current_balance,
                    "solscan_tx_url": None,
                }

            # Parse signature from output
            signature = None
            for line in result.stdout.split("\n"):
                if "Signature" in line:
                    signature = line.split()[-1]
                    break

            # Get new balance
            _, new_balance = get_wallet_balances(wallet_address)
            new_balance = new_balance or 0.0

            await ctx.info(
                f"Withdrew {amount:.2f} $BOTVEGAS to {request.destination_address[:8]}..."
            )

            return {
                "success": True,
                "signature": signature,
                "amount_sent": amount,
                "destination": request.destination_address,
                "remaining_balance": new_balance,
                "solscan_tx_url": f"https://solscan.io/tx/{signature}"
                if signature
                else None,
            }

        finally:
            try:
                os.unlink(keypair_path)
            except Exception:
                pass

    except Exception as e:
        logger.error(f"Withdrawal error: {e}")
        return {
            "success": False,
            "error": str(e),
            "signature": None,
            "amount_sent": 0,
            "destination": request.destination_address,
            "remaining_balance": 0,
            "solscan_tx_url": None,
        }


@mcp.tool()
async def botvegas_list_agent_wallets(ctx: Context) -> dict[str, Any]:
    """List all agent wallets stored on this machine.

    Shows each identity's wallet address and balances.
    Useful for managing multiple agents.

    Returns:
    - List of wallets with addresses and balances
    - Total SOL across all wallets
    - Total $BOTVEGAS across all wallets
    """
    logger.info("Listing all agent wallets")

    try:
        result = list_agent_wallets()
        await ctx.info(
            f"Found {result['wallet_count']} wallet(s), "
            f"total: {result['total_sol']:.4f} SOL, {result['total_botvegas']:.2f} $BOTVEGAS"
        )
        return result

    except Exception as e:
        logger.error(f"List wallets error: {e}")
        return {
            "error": str(e),
            "wallets": [],
            "total_sol": 0,
            "total_botvegas": 0,
            "wallet_count": 0,
        }


# =============================================================================
# Tools - Transaction Signing (Deposit/Withdraw)
# =============================================================================


class DepositToEscrowRequest(BaseModel):
    """Request to deposit tokens to game escrow."""

    identity_name: str = Field(
        ...,
        min_length=3,
        max_length=50,
        description="Identity whose wallet to use for the deposit.",
    )
    token: str = Field(
        ...,
        description="Player's authentication token (for API calls).",
    )
    game_id: str = Field(
        ...,
        description="UUID of the staked game to deposit into.",
    )
    buy_in_amount: float = Field(
        ...,
        gt=0,
        description="Amount of $BOTVEGAS to deposit (must match game buy-in).",
    )


class WithdrawFromEscrowRequest(BaseModel):
    """Request to withdraw tokens from game escrow."""

    identity_name: str = Field(
        ...,
        min_length=3,
        max_length=50,
        description="Identity whose wallet to use for the withdrawal.",
    )
    token: str = Field(
        ...,
        description="Player's authentication token (for API calls).",
    )
    game_id: str = Field(
        ...,
        description="UUID of the staked game to withdraw from.",
    )


@mcp.tool()
async def botvegas_deposit_to_escrow(
    request: DepositToEscrowRequest, ctx: Context
) -> dict[str, Any]:
    """Deposit $BOTVEGAS tokens into a staked game's escrow.

    This tool:
    1. Gets the deposit instruction from the BotVegas backend
    2. Builds a Solana transaction with the instruction
    3. Signs it with your agent's local keypair
    4. Submits it to the Solana network
    5. Waits for confirmation

    Prerequisites:
    - Agent must have a wallet set up (botvegas_setup_wallet)
    - Wallet must have enough SOL for fees (~0.01 SOL)
    - Wallet must have enough $BOTVEGAS for the buy-in
    - Wallet address must be registered with identity (botvegas_set_wallet)

    After successful deposit, the agent can join the game and play.

    Returns:
    - Transaction signature
    - Amount deposited
    - Updated wallet balances
    - Solscan link to view transaction
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(
        f"Depositing {request.buy_in_amount} $BOTVEGAS to game {request.game_id} "
        f"for identity {request.identity_name}"
    )

    try:
        # Step 1: Get deposit instruction from backend
        await ctx.info("Getting deposit instruction from BotVegas...")
        instruction_data = await client.get_deposit_instruction(
            token=request.token,
            game_id=request.game_id,
        )

        if "error" in instruction_data:
            return {
                "success": False,
                "error": f"Failed to get deposit instruction: {instruction_data['error']}",
                "signature": None,
                "amount_deposited": 0,
                "game_id": request.game_id,
                "solscan_url": None,
            }

        # Step 2: Execute deposit (build, sign, submit)
        await ctx.info("Building and signing transaction...")
        result = await execute_deposit(
            identity_name=request.identity_name,
            instruction_data=instruction_data,
            buy_in_amount=request.buy_in_amount,
        )

        if result.success:
            await ctx.info(
                f"✅ Deposit successful! Signature: {result.signature[:16]}..."
            )
        else:
            await ctx.info(f"❌ Deposit failed: {result.error}")

        return result.to_dict()

    except BotVegasAPIError as e:
        logger.error(f"API error during deposit: {e}")
        return {
            "success": False,
            "error": f"API error: {e.message}",
            "signature": None,
            "amount_deposited": 0,
            "wallet_address": None,
            "game_id": request.game_id,
            "solscan_url": None,
            "new_sol_balance": None,
            "new_botvegas_balance": None,
        }
    except Exception as e:
        logger.error(f"Deposit error: {e}")
        return {
            "success": False,
            "error": str(e),
            "signature": None,
            "amount_deposited": 0,
            "wallet_address": None,
            "game_id": request.game_id,
            "solscan_url": None,
            "new_sol_balance": None,
            "new_botvegas_balance": None,
        }


@mcp.tool()
async def botvegas_withdraw_from_escrow(
    request: WithdrawFromEscrowRequest, ctx: Context
) -> dict[str, Any]:
    """Withdraw $BOTVEGAS tokens from a staked game's escrow.

    Use this after a game has ended to collect your winnings (or refund).

    This tool:
    1. Gets the withdraw instruction from the BotVegas backend
    2. Builds a Solana transaction with the instruction
    3. Signs it with your agent's local keypair
    4. Submits it to the Solana network
    5. Waits for confirmation

    Prerequisites:
    - Game must be in 'settled' or 'refund' status
    - Agent must have deposited to this game
    - Agent must have enough SOL for transaction fees

    Returns:
    - Transaction signature
    - Amount withdrawn (winnings or refund)
    - Updated wallet balances
    - Solscan link to view transaction
    """
    client = ctx.request_context.lifespan_context.client
    logger.info(
        f"Withdrawing from game {request.game_id} for identity {request.identity_name}"
    )

    try:
        # Step 1: Get withdraw instruction from backend
        await ctx.info("Getting withdraw instruction from BotVegas...")
        instruction_data = await client.get_withdraw_instruction(
            token=request.token,
            game_id=request.game_id,
        )

        if "error" in instruction_data:
            return {
                "success": False,
                "error": f"Failed to get withdraw instruction: {instruction_data['error']}",
                "signature": None,
                "amount_withdrawn": 0,
                "game_id": request.game_id,
                "solscan_url": None,
            }

        # Step 2: Execute withdraw (build, sign, submit)
        await ctx.info("Building and signing transaction...")
        result = await execute_withdraw(
            identity_name=request.identity_name,
            instruction_data=instruction_data,
        )

        if result.success:
            await ctx.info(
                f"✅ Withdrawal successful! Signature: {result.signature[:16]}..."
            )
        else:
            await ctx.info(f"❌ Withdrawal failed: {result.error}")

        return result.to_dict()

    except BotVegasAPIError as e:
        logger.error(f"API error during withdraw: {e}")
        return {
            "success": False,
            "error": f"API error: {e.message}",
            "signature": None,
            "amount_withdrawn": 0,
            "wallet_address": None,
            "game_id": request.game_id,
            "solscan_url": None,
            "new_sol_balance": None,
            "new_botvegas_balance": None,
        }
    except Exception as e:
        logger.error(f"Withdraw error: {e}")
        return {
            "success": False,
            "error": str(e),
            "signature": None,
            "amount_withdrawn": 0,
            "wallet_address": None,
            "game_id": request.game_id,
            "solscan_url": None,
            "new_sol_balance": None,
            "new_botvegas_balance": None,
        }


# =============================================================================
# Main Entry Point
# =============================================================================


def main():
    """Run the MCP server."""
    mcp.run()


if __name__ == "__main__":
    main()
