
#!/usr/bin/env python3
"""
GHL Location Update Script (S-K103)

Production-ready Python script to update GoHighLevel (GHL) location details.
Updates location name to "Bunker FNQ" and address to "Kuranda QLD 4881".

Features:
    - Type-hinted with Pydantic validation
    - Comprehensive error handling with custom exceptions
    - Exponential backoff retry logic for transient failures
    - Structured logging
    - Environment-based configuration
    - Context manager for resource cleanup

Environment Variables:
    GHL_API_KEY (required): Bearer token for GHL API authentication
    GHL_LOCATION_ID (required): UUID of the location to update
    GHL_BASE_URL (optional): API base URL (default: https://rest.gohighlevel.com/v1)
    LOG_LEVEL (optional): DEBUG, INFO, WARNING, ERROR (default: INFO)

Usage:
    export GHL_API_KEY="your-api-key"
    export GHL_LOCATION_ID="your-location-id"
    python ghl_location_update.py

Exit Codes:
    0: Success
    1: Configuration error (missing env vars, invalid config)
    2: API error (authentication, not found, validation errors)
    3: Unexpected error
"""

import os
import sys
import json
import logging
from typing import Dict, Any, Optional, Final
from dataclasses import dataclass
from enum import IntEnum
import httpx
from pydantic import BaseModel, Field, ValidationError, validator
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
    before_sleep_log,
    RetryError
)

# Constants
DEFAULT_BASE_URL: Final[str] = "https://rest.gohighlevel.com/v1"
MAX_RETRIES: Final[int] = 3
REQUIRED_ENV_VARS: Final[list] = ["GHL_API_KEY", "GHL_LOCATION_ID"]


# Logging Configuration
def configure_logging(log_level: Optional[str] = None) -> logging.Logger:
    """
    Configure structured logging with timestamps and log levels.
    
    Args:
        log_level: Optional override for log level (defaults to env var LOG_LEVEL or INFO)
        
    Returns:
        Configured logger instance
    """
    level = log_level or os.getenv("LOG_LEVEL", "INFO").upper()
    
    logging.basicConfig(
        level=getattr(logging, level, logging.INFO),
        format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )
    return logging.getLogger("ghl_location_updater")


logger = configure_logging()


class ExitCode(IntEnum):
    """Standardized exit codes for shell integration and CI/CD pipelines."""
    SUCCESS = 0
    CONFIG_ERROR = 1
    API_ERROR = 2
    UNEXPECTED_ERROR = 3


class GHLAPIError(Exception):
    """
    Custom exception for GHL API errors.
    
    Attributes:
        status_code: HTTP status code returned by API
        message: Error message from API or exception
        response_body: Raw response body for debugging
    """
    
    def __init__(self, status_code: int, message: str, response_body: Optional[str] = None):
        self.status_code = status_code
        self.message = message
        self.response_body = response_body
        super().__init__(f"GHL API Error [{status_code}]: {message}")


class ConfigurationError(Exception):
    """Raised when required configuration is missing or invalid."""
    pass


class LocationUpdatePayload(BaseModel):
    """
    Pydantic model for GHL Location Update API payload.
    
    Validates the request structure before sending to API.
    """
    name: str = Field(default="Bunker FNQ", description="Location business name")
    address: str = Field(default="Kuranda QLD 4881", description="Formatted business address")
    
    @validator('name')
    def name_must_not_be_empty(cls, v):
        if not v or not v.strip():
            raise ValueError('Location name cannot be empty')
        return v.strip()
    
    @validator('address')
    def address_must_not_be_empty(cls, v):
        if not v or not v.strip():
            raise ValueError('Address cannot be empty')
        return v.strip()
    
    class Config:
        """Pydantic configuration."""
        schema_extra = {
            "example": {
                "name": "Bunker FNQ",
                "address": "Kuranda QLD 4881"
            }
        }


class GHLConfig(BaseModel):
    """
    Configuration model for GHL API connection.
    
    Validates environment variables and provides typed configuration access.
    """
    api_key: str = Field(..., description="GHL API Bearer token")
    location_id: str = Field(..., description="UUID of location to update")
    base_url: str = Field(default=DEFAULT_BASE_URL, description="GHL API base URL")
    
    @validator('api_key')
    def api_key_must_be_valid(cls, v):
        if not v or len(v.strip()) < 10:  # Basic length check for API keys
            raise ValueError('API key appears invalid (too short or empty)')
        return v.strip()
    
    @validator('location_id')
    def location_id_must_be_valid(cls, v):
        if not v or not v.strip():
            raise ValueError('Location ID cannot be empty')
        return v.strip()
    
    @validator('base_url')
    def base_url_must_be_http(cls, v):
        if not v.startswith(('http://', 'https://')):
            raise ValueError('Base URL must start with http:// or https://')
        return v.rstrip('/')  # Remove trailing slash
    
    class Config:
        """Allow environment variable mapping."""
        env_prefix = 'GHL_'


class GHLLocationClient:
    """
    HTTP client for GHL Location API operations.
    
    Implements context manager protocol for proper resource cleanup.
    Includes automatic retry logic for transient failures (5xx errors, timeouts).
    """
    
    def __init__(self, config: GHLConfig):
        """
        Initialize client with configuration.
        
        Args:
            config: Validated GHLConfig instance
            
        Raises:
            ConfigurationError: If config is invalid
        """
        self.config = config
        self._client: Optional[httpx.Client] = None
        self._setup_client()
        
    def _setup_client(self) -> None:
        """Configure HTTP client with authentication headers."""
        headers = {
            "Authorization": f"Bearer {self.config.api_key}",
            "Content-Type": "application/json",
            "Accept": "application/json",
            "User-Agent": "GHL-Location-Updater/1.0"
        }
        
        self._client = httpx.Client(
            base_url=self.config.base_url,
            headers=headers,
            timeout=httpx.Timeout(30.0, connect=10.0),
            follow_redirects=True
        )
        logger.debug(f"HTTP client initialized for {self.config.base_url}")
    
    @retry(
        retry=retry_if_exception_type((httpx.HTTPStatusError, httpx.TimeoutException, httpx.ConnectError)),
        stop=stop_after_attempt(MAX_RETRIES),
        wait=wait_exponential(multiplier=1, min=2, max=10),
        before_sleep=before_sleep_log(logger, logging.WARNING),
        reraise=True
    )
    def update_location(self, payload: LocationUpdatePayload) -> Dict[str, Any]:
        """
        Update GHL location details via API.
        
        Args:
            payload: LocationUpdatePayload with name and address
            
        Returns:
            Dict containing API response data
            
        Raises:
            GHLAPIError: When API returns 4xx/5xx errors
            RetryError: When max retries exceeded for transient errors
        """
        if not self._client:
            raise RuntimeError("HTTP client not initialized")
            
        endpoint = f"/locations/{self.config.location_id}"
        
        try:
            logger.info(f"Updating location {self.config.location_id} with payload: {payload.json()}")
            
            response = self._client.put(
                endpoint,
                json=payload.dict()
            )
            
            # Raise for 4xx/5xx status codes (triggers retry if applicable)
            response.raise_for_status()
            
            data = response.json()
            logger.info(f"Successfully updated location {self.config.location_id}")
            logger.debug(f"API Response: {json.dumps(data, indent=2)}")
            
            return data
            
        except httpx.HTTPStatusError as e:
            # Handle specific HTTP error codes
            status_code = e.response.status_code
            try:
                error_body = e.response.json()
                error_msg = error_body.get('message', str(e))
            except json.JSONDecodeError:
                error_body = e.response.text
                error_msg = str(e)
            
            # Don't retry client errors (4xx) except 429 (rate limit)
            if status_code == 401:
                raise GHLAPIError(401, "Authentication failed - invalid API key", error_body)
            elif status_code == 403:
                raise GHLAPIError(403, "Forbidden - insufficient permissions", error_body)
            elif status_code == 404:
                raise GHLAPIError(404, f"Location {self.config.location_id} not found", error_body)
            elif status_code == 422:
                raise GHLAPIError(422, f"Validation error: {error_msg}", error_body)
            elif status_code == 429:
                # Rate limit - allow retry
                logger.warning("Rate limit hit, retrying...")
                raise
            else:
                # Server errors (5xx) will be retried by decorator
                raise GHLAPIError(status_code, f"HTTP Error: {error_msg}", error_body)
                
        except httpx.TimeoutException:
            logger.error("Request timed out")
            raise
        except httpx.ConnectError:
            logger.error("Connection error")
            raise
    
    def close(self) -> None:
        """Close HTTP client connections."""
        if self._client:
            self._client.close()
            self._client = None
            logger.debug("HTTP client closed")
    
    def __enter__(self) -> 'GHLLocationClient':
        """Context manager entry."""
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """Context manager exit with cleanup."""
        self.close()


def load_configuration() -> GHLConfig:
    """
    Load and validate configuration from environment variables.
    
    Returns:
        Validated GHLConfig instance
        
    Raises:
        ConfigurationError: If required env vars missing or invalid