# Utils - Shared Utilities and Helpers

## Overview

Centralized utility functions and helpers used across the backend application. Provides logging configuration, authentication utilities, and shared helper functions.

## Purpose

- **Logging Setup**: Centralized logging configuration with rotating file handlers
- **Authentication**: JWT token creation and verification
- **Shared Helpers**: Common utility functions used throughout the application

## Module Structure

```
app/utils/
├── __init__.py          # Package marker
├── logging.py           # Logging configuration (rotating handlers)
└── auth.py              # JWT authentication utilities
```

## Key Components

### 1. Logging Module (`logging.py`)

Configures application-wide logging with both console and file handlers.

#### Features

**Rotating File Handlers**:
- Daily log files: `logs/app_YYYYMMDD.log`
- Max file size: 10MB per file
- Keep last 5 backup files
- Format: `YYYY-MM-DD HH:MM:SS - module.name - LEVEL - message`

**Dual Output**:
- Console (INFO level): For development monitoring
- File (DEBUG level): For detailed debugging

**Noisy Logger Silencing**:
- SQLAlchemy engine: WARNING level only
- Socket.io: INFO level only
- Engine.io: WARNING level only

#### Usage

**Application Startup** (`app/main.py`):
```python
from app.utils.logging import setup_logging

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Startup and shutdown events"""
    logger.info("Starting Paper Dynasty Game Backend")
    setup_logging()  # Configure logging on startup
    await init_db()
    yield
    logger.info("Shutting down Paper Dynasty Game Backend")
```

**Module-Level Loggers** (Standard Pattern):
```python
import logging

# For classes - include class name
logger = logging.getLogger(f'{__name__}.ClassName')

# For modules without classes - just module name
logger = logging.getLogger(f'{__name__}')

# Usage examples
logger.info("User connected")
logger.warning("Invalid request received")
logger.error("Failed to process action", exc_info=True)
logger.debug("Processing state transition")
```

**Logging Pattern Examples Across Codebase**:
```python
# In game_engine.py
logger = logging.getLogger(f'{__name__}.GameEngine')

# In state_manager.py
logger = logging.getLogger(f'{__name__}.StateManager')

# In database operations
logger = logging.getLogger(f'{__name__}.DatabaseOperations')

# In WebSocket handlers
logger = logging.getLogger(f'{__name__}.handlers')

# In API routes
logger = logging.getLogger(f'{__name__}.games')
```

#### Configuration Details

**Log Directory**:
- Location: `backend/logs/` (auto-created)
- Gitignored: Yes
- Naming: `app_YYYYMMDD.log` using Pendulum for UTC timestamps

**Log Levels**:
- Root logger: DEBUG (captures everything)
- Console handler: INFO (important messages only)
- File handler: DEBUG (full detail)
- SQLAlchemy engine: WARNING (reduce noise)
- Socket.io/Engine.io: INFO/WARNING (reduce noise)

**Rotation**:
- Size-based: 10MB max file size
- Time-based: Daily file names with UTC timestamps
- Backup count: Keep 5 most recent files
- Older logs automatically deleted

**Log Format**:
```
2025-10-31 14:23:45 - app.core.game_engine.GameEngine - INFO - Starting new game abc123
2025-10-31 14:23:46 - app.database.operations.DatabaseOperations - DEBUG - Executing query: INSERT INTO games...
2025-10-31 14:23:47 - app.websocket.handlers - ERROR - Connection failed: timeout
```

#### Implementation Details

```python
def setup_logging() -> None:
    """Configure application logging"""

    # Create logs directory
    log_dir = "logs"
    os.makedirs(log_dir, exist_ok=True)

    # Log file name with date (Pendulum for UTC)
    now = pendulum.now('UTC')
    log_file = os.path.join(log_dir, f"app_{now.format('YYYYMMDD')}.log")

    # Formatter (consistent across handlers)
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )

    # Console handler (INFO for development)
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(formatter)

    # Rotating file handler (DEBUG for full detail)
    file_handler = logging.handlers.RotatingFileHandler(
        log_file,
        maxBytes=10 * 1024 * 1024,  # 10MB
        backupCount=5
    )
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(formatter)

    # Configure root logger
    root_logger = logging.getLogger()
    root_logger.setLevel(logging.DEBUG)
    root_logger.addHandler(console_handler)
    root_logger.addHandler(file_handler)

    # Silence noisy loggers
    logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
    logging.getLogger("socketio").setLevel(logging.INFO)
    logging.getLogger("engineio").setLevel(logging.WARNING)
```

### 2. Authentication Module (`auth.py`)

JWT token creation and verification for user authentication.

#### Features

**Token Creation**:
- JWT with HS256 algorithm
- 7-day expiration (configurable)
- Embeds arbitrary user data
- Uses Pendulum for UTC timestamps

**Token Verification**:
- Validates signature and expiration
- Returns decoded payload
- Raises `JWTError` on failure

#### Dependencies

```python
from jose import jwt, JWTError  # Python-JOSE library
import pendulum                  # UTC timestamps
from app.config import get_settings  # Secret key from settings
```

#### Usage

**Creating Tokens**:
```python
from app.utils.auth import create_token

# Create token with user data
user_data = {
    "user_id": 123,
    "username": "player1",
    "discord_id": "987654321",
    "league": "sba"
}
token = create_token(user_data)
# Returns: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```

**Verifying Tokens**:
```python
from app.utils.auth import verify_token
from jose import JWTError

try:
    payload = verify_token(token)
    user_id = payload["user_id"]
    username = payload["username"]
except JWTError:
    # Token invalid or expired
    return {"error": "Invalid token"}
```

**Integration with Discord OAuth** (`app/api/routes/auth.py`):
```python
from app.utils.auth import create_token

@router.post("/discord/callback")
async def discord_callback(code: str):
    # Exchange code for Discord token
    discord_token = await exchange_code_for_token(code)

    # Get Discord user info
    user_info = await get_discord_user(discord_token)

    # Create JWT for our application
    token = create_token({
        "user_id": user_info["id"],
        "username": user_info["username"],
        "discord_id": user_info["id"]
    })

    return {"token": token}
```

**WebSocket Authentication** (`app/websocket/handlers.py`):
```python
from app.utils.auth import verify_token
from jose import JWTError

@sio.event
async def connect(sid, environ, auth):
    """Authenticate WebSocket connection"""
    try:
        # Verify token from auth dict
        token = auth.get("token")
        if not token:
            raise ConnectionRefusedError("Missing token")

        payload = verify_token(token)

        # Store user info in session
        await sio.save_session(sid, {
            "user_id": payload["user_id"],
            "username": payload["username"]
        })

        logger.info(f"User {payload['username']} connected")
    except JWTError:
        logger.warning("Invalid token on connection attempt")
        raise ConnectionRefusedError("Invalid token")
```

#### Implementation Details

**Token Creation**:
```python
def create_token(user_data: Dict[str, Any]) -> str:
    """
    Create JWT token for user

    Args:
        user_data: User information to encode in token

    Returns:
        JWT token string
    """
    payload = {
        **user_data,
        "exp": pendulum.now('UTC').add(days=7).int_timestamp
    }
    token = jwt.encode(payload, settings.secret_key, algorithm="HS256")
    return token
```

**Token Verification**:
```python
def verify_token(token: str) -> Dict[str, Any]:
    """
    Verify and decode JWT token

    Args:
        token: JWT token string

    Returns:
        Decoded token payload

    Raises:
        JWTError: If token is invalid or expired
    """
    try:
        payload = jwt.decode(token, settings.secret_key, algorithms=["HS256"])
        return payload
    except JWTError as e:
        logger.warning(f"Invalid token: {e}")
        raise
```

**Configuration Requirements** (`.env`):
```bash
# Required for JWT signing
SECRET_KEY=your-secret-key-at-least-32-chars-long
```

## Patterns & Conventions

### Logging Pattern

**Standard Pattern** (Used Throughout Codebase):
```python
import logging

# Module-level logger with class name
logger = logging.getLogger(f'{__name__}.ClassName')

class ClassName:
    def some_method(self):
        logger.info("Method called")
        logger.debug("Processing details...")
        logger.error("Something went wrong", exc_info=True)
```

**Log Level Guidelines**:
- `DEBUG`: Detailed diagnostic information (state changes, query details)
- `INFO`: General informational messages (connections, game events)
- `WARNING`: Warning messages (invalid input, deprecations)
- `ERROR`: Error messages (exceptions, failures)
- `CRITICAL`: Critical failures (system-level issues)

**Exception Logging**:
```python
try:
    # operation
except Exception as e:
    logger.error(f"Operation failed: {e}", exc_info=True)
    raise
```

### Authentication Pattern

**CURRENT: HttpOnly Cookie Auth** (Server-Side OAuth Flow):
```python
# Backend sets cookies with path="/" for SSR compatibility
# See backend/app/utils/cookies.py and COOKIE_AUTH_IMPLEMENTATION.md

response.set_cookie(
    key="pd_access_token",
    value=access_token,
    httponly=True,
    secure=is_production(),
    samesite="lax",
    path="/",  # CRITICAL: Must be "/" not "/api" for SSR
)
```

**Frontend API Calls** (use `credentials: 'include'`):
```typescript
// Browser automatically sends cookies
const response = await $fetch('/api/games/', {
  credentials: 'include',
})
```

**SSR Cookie Forwarding** (Nuxt middleware must forward cookies):
```typescript
if (import.meta.server) {
  const event = useRequestEvent()
  headers['Cookie'] = event?.node.req.headers.cookie
}
```

**WebSocket Auth** (cookies sent automatically with same-origin):
```typescript
// Socket.io connects with cookies (same-origin)
const socket = io(wsUrl, {
  withCredentials: true,  // Send cookies
})
```

**LEGACY: Token in HTTP Headers** (no longer used for web frontend):
```python
# Only used for API clients that can't use cookies
headers = {
    "Authorization": f"Bearer {token}"
}
```

**FastAPI Dependency**:
```python
from fastapi import Depends, HTTPException, Header
from app.utils.auth import verify_token
from jose import JWTError

async def get_current_user(authorization: str = Header(None)):
    """Extract and verify user from Bearer token"""
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(401, "Missing or invalid authorization header")

    token = authorization.split(" ")[1]
    try:
        payload = verify_token(token)
        return payload
    except JWTError:
        raise HTTPException(401, "Invalid or expired token")

# Use in routes
@router.get("/protected")
async def protected_route(user = Depends(get_current_user)):
    return {"user_id": user["user_id"]}
```

## Integration Points

### Application Startup

**main.py** - Initialize logging on application startup:
```python
from app.utils.logging import setup_logging

@asynccontextmanager
async def lifespan(app: FastAPI):
    logger.info("Starting application")
    setup_logging()  # Must be called early
    await init_db()
    yield
    logger.info("Shutting down")
```

### Module Initialization

**Every Module with Logging**:
```python
import logging

logger = logging.getLogger(f'{__name__}.ClassName')
```

**Modules Using Auth**:
```python
from app.utils.auth import create_token, verify_token
from jose import JWTError
```

### WebSocket Handlers

**Connection Authentication**:
```python
from app.utils.auth import verify_token

@sio.event
async def connect(sid, environ, auth):
    token = auth.get("token")
    payload = verify_token(token)  # Raises JWTError if invalid
    # Store session data
```

### API Routes

**Protected Endpoints**:
```python
from app.utils.auth import verify_token

@router.get("/games")
async def get_games(authorization: str = Header(None)):
    token = authorization.split(" ")[1]
    user = verify_token(token)
    # Use user data
```

## Common Tasks

### Adding a New Utility Module

1. **Create Module File**:
```bash
touch app/utils/new_utility.py
```

2. **Implement Utility Functions**:
```python
# app/utils/new_utility.py
import logging

logger = logging.getLogger(f'{__name__}.NewUtility')

def utility_function(param: str) -> str:
    """
    Utility function description

    Args:
        param: Parameter description

    Returns:
        Return value description
    """
    logger.debug(f"Processing {param}")
    result = process(param)
    return result
```

3. **Export from __init__.py** (if needed for public API):
```python
# app/utils/__init__.py
from .logging import setup_logging
from .auth import create_token, verify_token
from .new_utility import utility_function

__all__ = [
    "setup_logging",
    "create_token",
    "verify_token",
    "utility_function"
]
```

4. **Add Tests**:
```python
# tests/unit/utils/test_new_utility.py
import pytest
from app.utils.new_utility import utility_function

def test_utility_function():
    result = utility_function("test")
    assert result == "expected"
```

### Modifying Logging Configuration

**Change Log Level**:
```python
# In setup_logging()
console_handler.setLevel(logging.DEBUG)  # Was INFO
```

**Add Custom Handler**:
```python
# In setup_logging()
# Add email handler for critical errors
from logging.handlers import SMTPHandler

email_handler = SMTPHandler(
    mailhost=("smtp.example.com", 587),
    fromaddr="alerts@example.com",
    toaddrs=["admin@example.com"],
    subject="Critical Error in Game Backend"
)
email_handler.setLevel(logging.CRITICAL)
email_handler.setFormatter(formatter)
root_logger.addHandler(email_handler)
```

**Change Rotation Policy**:
```python
# In setup_logging()
file_handler = logging.handlers.RotatingFileHandler(
    log_file,
    maxBytes=50 * 1024 * 1024,  # Change to 50MB
    backupCount=10              # Keep 10 backups
)
```

### Changing Token Expiration

**Modify create_token()**:
```python
# In auth.py
def create_token(user_data: Dict[str, Any]) -> str:
    payload = {
        **user_data,
        "exp": pendulum.now('UTC').add(days=30).int_timestamp  # 30 days instead of 7
    }
    token = jwt.encode(payload, settings.secret_key, algorithm="HS256")
    return token
```

### Adding Token Refresh

**Create refresh_token() utility**:
```python
# In auth.py
def refresh_token(old_token: str) -> str:
    """
    Refresh an existing token with new expiration

    Args:
        old_token: Existing valid token

    Returns:
        New token with extended expiration

    Raises:
        JWTError: If old token is invalid
    """
    # Verify old token
    payload = verify_token(old_token)

    # Remove old expiration
    payload.pop("exp", None)

    # Create new token
    return create_token(payload)
```

## Troubleshooting

### Logging Issues

**Problem**: No logs appearing
- **Check**: `setup_logging()` called in `main.py` lifespan?
- **Check**: `logs/` directory exists and is writable?
- **Check**: Using correct logger pattern: `logger = logging.getLogger(f'{__name__}.ClassName')`?

**Problem**: Too much SQLAlchemy noise in logs
- **Solution**: Already silenced in `setup_logging()` - check if being called
- **Alternative**: Increase SQLAlchemy logger level:
```python
logging.getLogger("sqlalchemy.engine").setLevel(logging.ERROR)
```

**Problem**: Logs not rotating
- **Check**: File size exceeding 10MB?
- **Check**: `RotatingFileHandler` configured correctly?
- **Debug**: Check `logs/` directory for backup files (`app_YYYYMMDD.log.1`, `.2`, etc.)

**Problem**: Daily log files not working
- **Solution**: Files are daily-named but don't auto-switch at midnight
- **Enhancement**: Use `TimedRotatingFileHandler` for true daily rotation:
```python
file_handler = logging.handlers.TimedRotatingFileHandler(
    "logs/app.log",
    when="midnight",
    interval=1,
    backupCount=30  # Keep 30 days
)
```

### Authentication Issues

**Problem**: `JWTError: Invalid token`
- **Check**: Token copied correctly (no whitespace)?
- **Check**: Token not expired (7-day limit)?
- **Check**: `SECRET_KEY` same on creation and verification?
- **Check**: Using correct algorithm (HS256)?

**Problem**: `ImportError: cannot import name 'jwt' from 'jose'`
- **Solution**: Install python-jose: `pip install python-jose[cryptography]`

**Problem**: Tokens working in Postman but not browser
- **Check**: CORS settings in `config.py`
- **Check**: Browser sending `Authorization: Bearer <token>` header?
- **Check**: Token not being stripped by middleware?

**Problem**: WebSocket auth failing
- **Check**: Token in `auth` object, not headers
- **Client Code**:
```javascript
const socket = io("http://localhost:8000", {
    auth: {
        token: "YOUR_TOKEN_HERE"
    }
});
```

### Pendulum Date Issues

**Problem**: `AttributeError: 'DateTime' object has no attribute 'int_timestamp'`
- **Check**: Using Pendulum 3.0+?
- **Solution**: `int_timestamp` is Pendulum 3.0 feature
- **Alternative** (Pendulum 2.x):
```python
"exp": int(pendulum.now('UTC').add(days=7).timestamp())
```

## Security Considerations

### JWT Security

**Secret Key Requirements**:
- Minimum 32 characters
- Cryptographically random
- Never commit to git
- Rotate periodically

**Generate Secure Key**:
```bash
# Using Python
python -c "import secrets; print(secrets.token_urlsafe(32))"

# Using OpenSSL
openssl rand -base64 32
```

**Token Best Practices**:
- Always use HTTPS in production (tokens visible in HTTP)
- Short expiration times (7 days or less)
- Implement refresh token flow for long-lived sessions
- Never log token values
- Validate all claims on decode

### Logging Security

**Never Log**:
- Passwords or secrets
- Complete JWT tokens
- Database credentials
- API keys
- User personal information

**Safe Logging**:
```python
# ❌ DON'T
logger.info(f"User logged in with token: {token}")

# ✅ DO
logger.info(f"User {user_id} logged in")
logger.debug(f"Token prefix: {token[:10]}...")
```

**Log Sanitization**:
```python
def sanitize_for_log(data: dict) -> dict:
    """Remove sensitive fields before logging"""
    sensitive_keys = ["password", "token", "secret", "api_key"]
    return {k: "***" if k in sensitive_keys else v for k, v in data.items()}

logger.info(f"Request data: {sanitize_for_log(request_data)}")
```

## Testing

### Unit Tests for Utilities

**Test Structure**:
```
tests/unit/utils/
├── test_logging.py      # Logging configuration tests
└── test_auth.py         # JWT creation/verification tests
```

**Example: Testing Token Creation**:
```python
# tests/unit/utils/test_auth.py
import pytest
from app.utils.auth import create_token, verify_token
from jose import JWTError

def test_create_token():
    """Test JWT token creation"""
    user_data = {"user_id": 123, "username": "test"}
    token = create_token(user_data)

    assert token is not None
    assert isinstance(token, str)
    assert len(token) > 0

def test_verify_token_valid():
    """Test verifying valid token"""
    user_data = {"user_id": 123, "username": "test"}
    token = create_token(user_data)

    payload = verify_token(token)
    assert payload["user_id"] == 123
    assert payload["username"] == "test"
    assert "exp" in payload

def test_verify_token_invalid():
    """Test verifying invalid token"""
    with pytest.raises(JWTError):
        verify_token("invalid.token.here")

def test_token_expiration():
    """Test token expiration"""
    # Would need to mock time or wait 7 days
    # Use freezegun or similar for time-based tests
    pass
```

**Example: Testing Logging Setup**:
```python
# tests/unit/utils/test_logging.py
import pytest
import logging
import os
from app.utils.logging import setup_logging

def test_setup_logging_creates_directory(tmp_path, monkeypatch):
    """Test that setup_logging creates logs directory"""
    monkeypatch.chdir(tmp_path)
    setup_logging()
    assert os.path.exists("logs")

def test_setup_logging_configures_handlers():
    """Test that handlers are configured"""
    setup_logging()
    root_logger = logging.getLogger()

    assert len(root_logger.handlers) >= 2
    assert any(isinstance(h, logging.StreamHandler) for h in root_logger.handlers)
    assert any(isinstance(h, logging.handlers.RotatingFileHandler) for h in root_logger.handlers)

def test_setup_logging_sets_levels():
    """Test that log levels are set correctly"""
    setup_logging()

    assert logging.getLogger("sqlalchemy.engine").level == logging.WARNING
    assert logging.getLogger("socketio").level == logging.INFO
    assert logging.getLogger("engineio").level == logging.WARNING
```

## Examples

### Complete Authentication Flow

```python
# 1. User logs in via Discord OAuth
@router.post("/auth/discord/callback")
async def discord_callback(code: str):
    # Exchange code for Discord access token
    discord_token = await exchange_discord_code(code)

    # Get Discord user info
    user_info = await get_discord_user(discord_token)

    # Create our JWT token
    token = create_token({
        "user_id": user_info["id"],
        "username": user_info["username"],
        "discriminator": user_info["discriminator"]
    })

    return {"token": token}

# 2. Client stores token and uses for API calls
# Client code (JavaScript):
# localStorage.setItem("token", response.token)

# 3. Client connects to WebSocket with token
@sio.event
async def connect(sid, environ, auth):
    try:
        token = auth.get("token")
        payload = verify_token(token)

        await sio.save_session(sid, {
            "user_id": payload["user_id"],
            "username": payload["username"]
        })

        logger.info(f"User {payload['username']} connected")
    except JWTError:
        raise ConnectionRefusedError("Invalid token")

# 4. Client makes authenticated API requests
@router.get("/games/my-games")
async def get_my_games(authorization: str = Header(None)):
    if not authorization:
        raise HTTPException(401, "Missing authorization")

    token = authorization.replace("Bearer ", "")
    payload = verify_token(token)

    user_id = payload["user_id"]
    games = await db.get_games_for_user(user_id)
    return games
```

### Custom Logger with Context

```python
import logging
from contextvars import ContextVar

# Context variable for request ID
request_id_var: ContextVar[str] = ContextVar("request_id", default="")

class ContextFilter(logging.Filter):
    """Add request ID to log records"""
    def filter(self, record):
        record.request_id = request_id_var.get()
        return True

def setup_logging():
    """Enhanced logging with request context"""
    # Standard setup...
    formatter = logging.Formatter(
        '%(asctime)s - %(request_id)s - %(name)s - %(levelname)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )

    # Add context filter
    context_filter = ContextFilter()
    console_handler.addFilter(context_filter)
    file_handler.addFilter(context_filter)

    # ... rest of setup

# In middleware/handler
import uuid
from contextvars import ContextVar

@app.middleware("http")
async def request_id_middleware(request, call_next):
    request_id = str(uuid.uuid4())
    request_id_var.set(request_id)
    response = await call_next(request)
    return response

# Now all logs in that request context include request_id
```

## Performance Considerations

### Logging Performance

**Avoid String Formatting Before Logging**:
```python
# ❌ DON'T - always formats even if not logged
logger.debug("Processing: " + expensive_operation())

# ✅ DO - only formats if debug enabled
logger.debug("Processing: %s", expensive_operation())

# ✅ ALSO GOOD - f-strings are evaluated lazily in some cases
logger.debug(f"Processing: {expensive_operation()}")  # But still evaluates
```

**Use Lazy Evaluation**:
```python
if logger.isEnabledFor(logging.DEBUG):
    expensive_data = generate_debug_info()
    logger.debug(f"Debug info: {expensive_data}")
```

### Token Verification Performance

**Cache Decoded Tokens** (for repeated verifications):
```python
from functools import lru_cache

@lru_cache(maxsize=1000)
def verify_token_cached(token: str) -> Dict[str, Any]:
    """Cached token verification (use with caution)"""
    return verify_token(token)

# Clear cache periodically or on logout
verify_token_cached.cache_clear()
```

**Note**: Be careful with caching - can't invalidate specific tokens, and cache may return expired tokens if not cleared.

## Related Documentation

- **Main Backend CLAUDE.md**: `../CLAUDE.md` - Application architecture
- **Config Module**: `../config/CLAUDE.md` - Settings and configuration
- **WebSocket Module**: `../websocket/CLAUDE.md` - WebSocket integration
- **API Routes**: `../api/routes/CLAUDE.md` - REST endpoint patterns

---

**Key Principles**:
1. **Centralized Logging**: All logging configuration in one place
2. **Consistent Patterns**: Use `f'{__name__}.ClassName'` everywhere
3. **Security First**: Never log sensitive data, use strong secret keys
4. **Simple Utilities**: Small, focused, reusable functions
5. **Type Safety**: Full type hints on all public functions

**Testing**: All utilities should have comprehensive unit tests covering normal and edge cases.
