# BaoLife Error Handling System

Comprehensive error handling for BaoLife with custom exceptions, decorators, and client notifications.

## Overview

This module provides:
- **Custom Exception Classes**: Typed errors with error codes and retry logic
- **Error Decorator**: Automatic error handling, logging, and client notification
- **Client Integration**: Seamless WebSocket error messaging
- **Structured Logging**: JSON-formatted error logs for production monitoring

## Quick Start

### Basic Usage

```python
from errors import handle_errors, InsufficientResourcesError

@handle_errors(send_to_client=my_send_function)
async def purchase_item(player_id: int, item_id: str):
    player_money = get_player_money(player_id)
    item_cost = get_item_cost(item_id)

    if player_money < item_cost:
        raise InsufficientResourcesError('money', item_cost, player_money)

    # Process purchase...
    return {'success': True}
```

### WebSocket Integration

```python
from errors import handle_errors, send_error_to_client
from app import sendToUser
import json

async def send_to_client(player_id, message):
    """Wrapper for BaoLife's sendToUser."""
    websocket = get_websocket_for_player(player_id)
    await sendToUser(websocket, json.dumps(message))

@handle_errors(send_to_client=send_to_client)
async def handle_player_action(player_id: int, action: str):
    # Your game logic here
    pass
```

## Exception Classes

### `GameError`

Base exception for all game-related errors.

```python
from errors import GameError

raise GameError(
    message="Invalid action",
    error_code="INVALID_ACTION",
    retry_possible=False
)
```

**Attributes:**
- `message` (str): Human-readable error description
- `error_code` (str): Machine-readable error code
- `retry_possible` (bool): Whether operation can be retried

### `InsufficientResourcesError`

Raised when player lacks required resources.

```python
from errors import InsufficientResourcesError

# Check energy before action
if player.energy < required_energy:
    raise InsufficientResourcesError('energy', required_energy, player.energy)

# Check money
if player.money < cost:
    raise InsufficientResourcesError('money', cost, player.money)

# Check diamonds
if player.diamonds < diamond_cost:
    raise InsufficientResourcesError('diamonds', diamond_cost, player.diamonds)
```

**Attributes:**
- `resource_type` (str): Type of resource (energy, money, diamonds)
- `required` (float): Amount required
- `current` (float): Amount player has
- Automatically sets `retry_possible=True`

### `ServerError`

Raised for internal server errors.

```python
from errors import ServerError

try:
    result = database_operation()
except Exception as e:
    raise ServerError("Database operation failed", original_exception=e)
```

**Attributes:**
- `original_exception` (Exception): Wrapped exception for debugging
- Automatically sets `retry_possible=True`

## Error Decorator

The `@handle_errors` decorator provides automatic error handling.

### Features

- **Automatic Logging**: All errors logged with context (player ID, function name)
- **Client Notification**: Errors sent to client via WebSocket
- **Exception Re-raising**: Preserves error for calling code to handle
- **Async/Sync Support**: Works with both async and sync functions
- **Function Signature Preservation**: Maintains wrapped function metadata

### Parameters

- `send_to_client` (Callable, optional): Function to send errors to client
- `log_level` (str, default='ERROR'): Logging level ('ERROR', 'WARNING', 'INFO')

### Examples

#### Async Function with Client Notification

```python
@handle_errors(send_to_client=send_func)
async def process_purchase(player_id: int, item_id: str):
    # Your logic here
    pass
```

#### Sync Function with Just Logging

```python
@handle_errors()
def validate_input(player_id: int, data: dict):
    # Your validation logic
    pass
```

#### Custom Log Level

```python
@handle_errors(send_to_client=send_func, log_level='WARNING')
async def soft_validation(player_id: int):
    # Non-critical validation
    pass
```

## Client Error Format

Errors sent to client have this structure:

```json
{
  "type": "error",
  "error_code": "INSUFFICIENT_RESOURCES",
  "message": "Insufficient energy: need 100, have 50",
  "retry_possible": true,
  "timestamp": "2025-11-12T10:30:45.123456"
}
```

### Client Handling Example (JavaScript)

```javascript
// In main.js or client code
socket.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);

  if (data.type === 'error') {
    showErrorModal({
      title: 'Error',
      message: data.message,
      retryButton: data.retry_possible
    });

    // Log for debugging
    console.error(`[${data.error_code}] ${data.message}`);
  }
});
```

## Common Error Codes

| Code | Description | Retry Possible |
|------|-------------|----------------|
| `INSUFFICIENT_RESOURCES` | Player lacks required resource | Yes |
| `INVALID_ACTION` | Action not allowed or unknown | No |
| `INVALID_PRODUCT` | Unknown product/item ID | No |
| `SERVER_ERROR` | Internal server error | Yes |
| `PLAYER_NOT_FOUND` | Player doesn't exist | No |
| `CHARACTER_NOT_FOUND` | Character/NPC doesn't exist | No |
| `RATE_LIMIT_EXCEEDED` | Too many requests | Yes |
| `DUPLICATE_TRANSACTION` | Transaction already processed | No |
| `VALIDATION_FAILED` | Input validation failed | No |
| `UNEXPECTED_ERROR` | Uncaught exception | Yes |

## Integration Examples

### 1. Energy Purchase

```python
from errors import handle_errors, InsufficientResourcesError, ServerError

@handle_errors(send_to_client=send_to_client)
async def purchase_energy(player_id: int, amount: int):
    player = load_player(player_id)
    cost = amount * 10

    # Validate funds
    if player.money < cost:
        raise InsufficientResourcesError('money', cost, player.money)

    # Process purchase
    player.money -= cost
    player.energy += amount

    # Save to database
    try:
        save_player(player)
    except Exception as e:
        raise ServerError("Failed to save player", original_exception=e)

    return {'success': True, 'new_energy': player.energy}
```

### 2. Activity Validation

```python
@handle_errors(send_to_client=send_to_client)
async def start_activity(player_id: int, activity_type: str):
    player = load_player(player_id)

    requirements = {
        'gym': {'energy': 20, 'money': 10},
        'study': {'energy': 15, 'money': 0}
    }

    if activity_type not in requirements:
        raise GameError(
            f"Unknown activity: {activity_type}",
            error_code='INVALID_ACTIVITY',
            retry_possible=False
        )

    req = requirements[activity_type]

    # Validate energy
    if player.energy < req['energy']:
        raise InsufficientResourcesError('energy', req['energy'], player.energy)

    # Validate money
    if player.money < req['money']:
        raise InsufficientResourcesError('money', req['money'], player.money)

    # Start activity...
    return {'success': True}
```

### 3. Database Operations

```python
@handle_errors()
def load_player_safe(player_id: int):
    try:
        player = load_player(player_id)

        if not player:
            raise GameError(
                f"Player {player_id} not found",
                error_code='PLAYER_NOT_FOUND',
                retry_possible=False
            )

        return player

    except Exception as e:
        raise ServerError(
            f"Failed to load player {player_id}",
            original_exception=e
        )
```

### 4. Combining with Rate Limiting

```python
from monetization.validation import rate_limit

@rate_limit(max_calls=10, time_window=60)
@handle_errors(send_to_client=send_to_client)
async def process_diamond_purchase(player_id: int, product_id: str):
    # Validate product
    if product_id not in VALID_PRODUCTS:
        raise GameError(
            f"Invalid product: {product_id}",
            error_code='INVALID_PRODUCT',
            retry_possible=False
        )

    # Process purchase...
    return {'success': True}
```

## Testing

Run the comprehensive test suite:

```bash
cd ws
python -m pytest tests/test_error_handler.py -v
```

Test coverage includes:
- Custom exception creation and attributes
- Decorator wrapping sync/async functions
- Error message formatting
- Retry flags
- WebSocket integration
- Logging behavior
- Multiple error scenarios
- Integration patterns

## Logging

All errors are logged with structured data:

```python
# Log output example (JSON in production)
{
  "timestamp": "2025-11-12T10:30:45.123456",
  "level": "ERROR",
  "logger": "errors.error_handler",
  "message": "GameError in purchase_energy: Insufficient money: need 100, have 50",
  "user_id": 123,
  "error_code": "INSUFFICIENT_RESOURCES",
  "retry_possible": true,
  "function": "purchase_energy"
}
```

## Best Practices

### 1. Use Specific Exceptions

```python
# Good
raise InsufficientResourcesError('energy', 100, player.energy)

# Avoid
raise GameError("Not enough energy")
```

### 2. Set Retry Flags Correctly

```python
# Retryable - temporary condition
raise InsufficientResourcesError('money', cost, balance)

# Not retryable - permanent condition
raise GameError("Invalid player ID", "INVALID_ID", retry_possible=False)
```

### 3. Wrap Database/External Errors

```python
try:
    database_operation()
except Exception as e:
    raise ServerError("Database error", original_exception=e)
```

### 4. Use Decorator for Consistent Handling

```python
# Consistent error handling across all game functions
@handle_errors(send_to_client=send_func)
async def game_function(player_id: int):
    # Your logic
    pass
```

### 5. Log at Appropriate Levels

```python
# Critical errors - ERROR level (default)
@handle_errors()
async def critical_operation():
    pass

# Non-critical - WARNING level
@handle_errors(log_level='WARNING')
async def soft_validation():
    pass
```

## Migration Guide

### Updating Existing Code

**Before:**
```python
async def purchase_item(player_id, item_id):
    try:
        player = load_player(player_id)

        if player.money < cost:
            await send_error(player_id, "Not enough money")
            return {'success': False}

        # Process...
    except Exception as e:
        logger.error(f"Purchase failed: {e}")
        return {'success': False}
```

**After:**
```python
from errors import handle_errors, InsufficientResourcesError

@handle_errors(send_to_client=send_to_client)
async def purchase_item(player_id: int, item_id: str):
    player = load_player(player_id)

    if player.money < cost:
        raise InsufficientResourcesError('money', cost, player.money)

    # Process...
    return {'success': True}
```

## Performance

The error handling system is designed for minimal overhead:

- **Decorator Overhead**: < 1ms per call
- **Logging**: Async-safe, non-blocking
- **Client Notification**: Fire-and-forget, doesn't block game logic
- **Memory**: Minimal - exceptions cleaned up after handling

## Troubleshooting

### Error Not Sent to Client

Check that:
1. `send_to_client` function is provided to decorator
2. `player_id` is passed to function (first arg or `player_id` kwarg)
3. WebSocket connection is active

### Function Signature Lost

The decorator preserves function signatures via `@wraps`. If you see issues:
```python
# Check function metadata
print(my_function.__name__)  # Should be original name
print(my_function.__doc__)   # Should be original docstring
```

### Errors Not Logged

Verify logging is configured:
```python
from logging_config import setup_logging
setup_logging()  # Should be called on app startup
```

## Future Enhancements

Potential additions for future phases:
- Error aggregation/monitoring dashboard
- Client-side error replay for debugging
- Automatic retry logic for transient errors
- Error rate limiting to prevent spam
- Custom error middleware chain

## Support

For questions or issues with the error handling system:
1. Check integration examples in `errors/integration_example.py`
2. Review test cases in `tests/test_error_handler.py`
3. See logging in production for error patterns
