"""
Safe Scripting Engine for Customer Automations

Allows customers to write simple logic (if/else, loops, variables) while preventing:
- Infinite loops
- Excessive memory usage
- File system access
- Network access (except approved APIs)
- Code injection
- Resource exhaustion

Uses RestrictedPython for safe code execution with additional safety layers.
"""

import ast
import time
import sys
from typing import Any, Dict, List, Optional
from io import StringIO
from contextlib import redirect_stdout, redirect_stderr
import logging

logger = logging.getLogger(__name__)


class ResourceLimitExceeded(Exception):
    """Raised when script exceeds resource limits"""
    pass


class ScriptExecutionError(Exception):
    """Raised when script execution fails"""
    pass


class SafeScriptAPI:
    """
    Safe API that customer scripts can access.

    Only exposes whitelisted operations that interact with their own data.
    """

    def __init__(self, business, user, execution_context, scheduled_task=None):
        self.business = business
        self.user = user
        self.context = execution_context
        self.scheduled_task = scheduled_task  # ScheduledTask instance for whitelist checking
        self._api_call_count = 0
        self._max_api_calls = 50  # Prevent API spam

    def _check_api_limit(self):
        """Enforce API call limits"""
        self._api_call_count += 1
        if self._api_call_count > self._max_api_calls:
            raise ResourceLimitExceeded(f"API call limit exceeded ({self._max_api_calls} calls)")

    def get_appointments(self, **filters):
        """
        Get appointments for this business with comprehensive filtering.

        Supported filters:
            - status: Exact status match (SCHEDULED, COMPLETED, CANCELED, etc.)
            - status__in: List of statuses ['SCHEDULED', 'COMPLETED']

            DateTime comparisons (ISO format string 'YYYY-MM-DDTHH:MM:SS'):
            - start_time__gt, start_time__gte, start_time__lt, start_time__lte
            - end_time__gt, end_time__gte, end_time__lt, end_time__lte
            - created_at__gt, created_at__gte, created_at__lt, created_at__lte
            - updated_at__gt, updated_at__gte, updated_at__lt, updated_at__lte

            Date shortcuts (YYYY-MM-DD format):
            - start_date: Appointments starting on or after this date
            - end_date: Appointments starting on or before this date

            Related objects:
            - service_id: Filter by service ID
            - location_id: Filter by location ID
            - customer_id: Filter by customer ID (via participants)
            - resource_id: Filter by resource ID (via participants)

            Text search (case-insensitive):
            - title__icontains: Title contains text
            - notes__icontains: Notes contains text

            Numeric comparisons:
            - deposit_amount__gt, deposit_amount__gte, deposit_amount__lt, deposit_amount__lte
            - final_price__gt, final_price__gte, final_price__lt, final_price__lte

            Boolean helpers:
            - has_deposit: True = has deposit, False = no deposit
            - has_final_price: True = has final price, False = no final price

            Pagination:
            - limit: Maximum results (default: 100, max: 1000)

        Returns:
            List of appointment dictionaries with full data
        """
        self._check_api_limit()

        from .models import Event
        from django.utils import timezone
        from datetime import datetime
        from dateutil.parser import parse as parse_datetime

        queryset = Event.objects.all().select_related('service', 'location')

        # Helper to parse datetime strings
        def parse_dt(value):
            if isinstance(value, datetime):
                return value if timezone.is_aware(value) else timezone.make_aware(value)
            try:
                dt = parse_datetime(value)
                return dt if timezone.is_aware(dt) else timezone.make_aware(dt)
            except (ValueError, TypeError):
                # Try date-only format
                try:
                    dt = datetime.strptime(value, '%Y-%m-%d')
                    return timezone.make_aware(dt)
                except (ValueError, TypeError):
                    return None

        # Status filters
        if 'status' in filters:
            queryset = queryset.filter(status=filters['status'])
        if 'status__in' in filters:
            queryset = queryset.filter(status__in=filters['status__in'])

        # Legacy date filters (for backwards compatibility)
        if 'start_date' in filters:
            dt = parse_dt(filters['start_date'])
            if dt:
                queryset = queryset.filter(start_time__gte=dt)
        if 'end_date' in filters:
            dt = parse_dt(filters['end_date'])
            if dt:
                queryset = queryset.filter(start_time__lte=dt)

        # DateTime comparison filters
        datetime_fields = ['start_time', 'end_time', 'created_at', 'updated_at']
        comparison_ops = ['__gt', '__gte', '__lt', '__lte']

        for field in datetime_fields:
            for op in comparison_ops:
                key = f'{field}{op}'
                if key in filters:
                    dt = parse_dt(filters[key])
                    if dt:
                        queryset = queryset.filter(**{key: dt})

        # Related object filters
        if 'service_id' in filters:
            queryset = queryset.filter(service_id=filters['service_id'])
        if 'location_id' in filters:
            queryset = queryset.filter(location_id=filters['location_id'])

        # Participant-based filters
        if 'customer_id' in filters:
            queryset = queryset.filter(participants__user_id=filters['customer_id']).distinct()
        if 'resource_id' in filters:
            queryset = queryset.filter(participants__resource_id=filters['resource_id']).distinct()

        # Text search filters
        if 'title__icontains' in filters:
            queryset = queryset.filter(title__icontains=filters['title__icontains'])
        if 'notes__icontains' in filters:
            queryset = queryset.filter(notes__icontains=filters['notes__icontains'])

        # Numeric comparison filters
        numeric_fields = ['deposit_amount', 'final_price']
        for field in numeric_fields:
            for op in comparison_ops:
                key = f'{field}{op}'
                if key in filters:
                    queryset = queryset.filter(**{key: filters[key]})

        # Boolean helper filters
        if 'has_deposit' in filters:
            if filters['has_deposit']:
                queryset = queryset.filter(deposit_amount__isnull=False).exclude(deposit_amount=0)
            else:
                queryset = queryset.filter(deposit_amount__isnull=True) | queryset.filter(deposit_amount=0)
        if 'has_final_price' in filters:
            if filters['has_final_price']:
                queryset = queryset.filter(final_price__isnull=False)
            else:
                queryset = queryset.filter(final_price__isnull=True)

        # Enforce limits
        limit = min(filters.get('limit', 100), 1000)
        queryset = queryset[:limit]

        # Serialize to safe dictionaries with comprehensive data
        return [
            {
                'id': event.id,
                'title': event.title,
                'start_time': event.start_time.isoformat(),
                'end_time': event.end_time.isoformat(),
                'status': event.status,
                'notes': event.notes,
                'created_at': event.created_at.isoformat() if event.created_at else None,
                'updated_at': event.updated_at.isoformat() if event.updated_at else None,
                'service_id': event.service_id,
                'service_name': event.service.name if event.service else None,
                'location_id': event.location_id,
                'location_name': event.location.name if event.location else None,
                'deposit_amount': float(event.deposit_amount) if event.deposit_amount else None,
                'final_price': float(event.final_price) if event.final_price else None,
            }
            for event in queryset
        ]

    def get_customers(self, **filters):
        """
        Get customers for this business with comprehensive filtering.

        Supported filters:
            - id: Exact customer ID
            - email: Exact email match
            - email__icontains: Email contains text (case-insensitive)
            - name__icontains: Name contains text (case-insensitive)
            - has_email: True = has email, False = no email
            - has_phone: True = has phone, False = no phone
            - is_active: Filter by active status (default: True)
            - created_at__gte, created_at__lte: Filter by creation date
            - limit: Maximum results (default: 100, max: 1000)

        Returns:
            List of customer dictionaries with fields:
            - id, email, name, phone, first_name, last_name, is_active, created_at
        """
        self._check_api_limit()

        from smoothschedule.identity.users.models import User
        from dateutil.parser import parse as parse_datetime
        from django.utils import timezone

        queryset = User.objects.filter(role='customer')

        # ID filter
        if 'id' in filters:
            queryset = queryset.filter(id=filters['id'])

        # Email filters
        if 'email' in filters:
            queryset = queryset.filter(email=filters['email'])
        if 'email__icontains' in filters:
            queryset = queryset.filter(email__icontains=filters['email__icontains'])

        # Name filters (search across first_name, last_name, username)
        if 'name__icontains' in filters:
            from django.db.models import Q
            search = filters['name__icontains']
            queryset = queryset.filter(
                Q(first_name__icontains=search) |
                Q(last_name__icontains=search) |
                Q(username__icontains=search)
            )

        # Boolean helpers
        if 'has_email' in filters:
            if filters['has_email']:
                queryset = queryset.exclude(email='').exclude(email__isnull=True)
            else:
                queryset = queryset.filter(Q(email='') | Q(email__isnull=True))

        if 'has_phone' in filters:
            if filters['has_phone']:
                queryset = queryset.exclude(phone='').exclude(phone__isnull=True)
            else:
                queryset = queryset.filter(Q(phone='') | Q(phone__isnull=True))

        # Active status
        if 'is_active' in filters:
            queryset = queryset.filter(is_active=filters['is_active'])

        # DateTime filters
        def parse_dt(value):
            if isinstance(value, str):
                try:
                    dt = parse_datetime(value)
                    return dt if timezone.is_aware(dt) else timezone.make_aware(dt)
                except (ValueError, TypeError):
                    return None
            return value

        for op in ['__gte', '__lte', '__gt', '__lt']:
            key = f'created_at{op}'
            if key in filters:
                dt = parse_dt(filters[key])
                if dt:
                    queryset = queryset.filter(**{f'date_joined{op}': dt})

        limit = min(filters.get('limit', 100), 1000)
        queryset = queryset[:limit]

        return [
            {
                'id': user.id,
                'email': user.email,
                'name': user.get_full_name() or user.username,
                'first_name': user.first_name,
                'last_name': user.last_name,
                'phone': getattr(user, 'phone', ''),
                'is_active': user.is_active,
                'created_at': user.date_joined.isoformat() if user.date_joined else None,
            }
            for user in queryset
        ]

    def send_email(self, to, subject, body):
        """
        Send an email to a customer.

        Args:
            to: Email address or customer ID
            subject: Email subject
            body: Email body (plain text, may contain insertion codes)

        Returns:
            True if sent successfully

        Note: body can contain insertion codes like {business_name}, {customer_name}, etc.
        These are automatically populated from the execution context.
        """
        self._check_api_limit()

        from django.core.mail import send_mail
        from django.conf import settings

        # Resolve customer ID to email if needed
        if isinstance(to, int):
            from smoothschedule.identity.users.models import User
            try:
                user = User.objects.get(id=to)
                to = user.email
            except User.DoesNotExist:
                raise ScriptExecutionError(f"Customer {to} not found")

        # Validate email
        if not to or '@' not in to:
            raise ScriptExecutionError(f"Invalid email address: {to}")

        # Process insertion codes in subject and body if they contain f-string patterns
        # The insertion codes were already converted to {variable_name} format by template parser
        # We need to evaluate them as f-strings with the context variables
        try:
            # Get context variables for insertion codes
            context = self._get_insertion_context()

            # Process subject and body as f-strings
            if '{' in subject:
                subject = subject.format(**context)
            if '{' in body:
                body = body.format(**context)
        except KeyError as e:
            raise ScriptExecutionError(f"Unknown insertion code: {e}")
        except Exception as e:
            logger.warning(f"Error processing insertion codes: {e}")
            # Continue with unprocessed text if there's an error

        # Length limits
        if len(subject) > 200:
            raise ScriptExecutionError("Subject too long (max 200 characters)")
        if len(body) > 10000:
            raise ScriptExecutionError("Body too long (max 10,000 characters)")

        try:
            send_mail(
                subject=subject,
                message=body,
                from_email=settings.DEFAULT_FROM_EMAIL,
                recipient_list=[to],
                fail_silently=False,
            )
            return True
        except Exception as e:
            logger.error(f"Failed to send email: {e}")
            return False

    def log(self, message):
        """Log a message (for debugging)"""
        logger.info(f"[Customer Script] {message}")

    def _get_insertion_context(self) -> Dict[str, str]:
        """
        Get context variables for insertion codes.

        Returns dict with business info and date/time values that can be used
        in email templates via insertion codes.
        """
        from datetime import datetime

        # Get business info from tenant
        business_name = getattr(self.business, 'name', '') if self.business else ''
        business_email = getattr(self.business, 'contact_email', '') if self.business else ''
        business_phone = getattr(self.business, 'phone', '') if self.business else ''

        # Date/time values
        now = datetime.now()
        today_str = now.strftime('%Y-%m-%d')
        now_str = now.strftime('%Y-%m-%d %H:%M:%S')

        # Build context dict
        # These variable names match what _mark_insertions_for_runtime() produces
        context = {
            'business_name': business_name,
            'business_email': business_email,
            'business_phone': business_phone,
            'today': today_str,
            'now': now_str,
            # Appointment-specific fields (empty if not in appointment context)
            'customer_name': '',
            'customer_email': '',
            'appointment_time': '',
            'appointment_date': '',
            'appointment_service': '',
        }

        return context

    def _validate_url(self, url, method='GET'):
        """
        Validate URL against whitelist and check for SSRF attacks.

        Args:
            url: URL to validate
            method: HTTP method (GET, POST, PUT, PATCH, DELETE)

        Raises:
            ScriptExecutionError: If URL is not whitelisted or is unsafe
        """
        from urllib.parse import urlparse
        from .models import WhitelistedURL

        parsed = urlparse(url)

        # Prevent SSRF attacks - check localhost
        if parsed.hostname in ['localhost', '127.0.0.1', '0.0.0.0', '::1']:
            raise ScriptExecutionError("Cannot access localhost")

        # Prevent access to private IP ranges
        import ipaddress
        try:
            ip = ipaddress.ip_address(parsed.hostname)
            if ip.is_private or ip.is_loopback or ip.is_link_local:
                raise ScriptExecutionError("Cannot access private IP addresses")
        except ValueError:
            # Not an IP address, continue with domain validation
            pass

        # Check whitelist using database model
        if not WhitelistedURL.is_url_whitelisted(url, method, self.scheduled_task):
            raise ScriptExecutionError(
                f"URL '{url}' with method '{method}' is not whitelisted for this plugin. "
                f"Contact support at pluginaccess@smoothschedule.com to request whitelisting."
            )

    def http_get(self, url, headers=None):
        """
        Make an HTTP GET request to approved domains.

        Args:
            url: URL to fetch (must be whitelisted)
            headers: Optional headers dictionary

        Returns:
            Response text

        Raises:
            ScriptExecutionError: If URL not whitelisted or request fails
        """
        self._check_api_limit()
        self._validate_url(url, 'GET')

        import requests

        try:
            response = requests.get(
                url,
                headers=headers or {},
                timeout=10,  # 10 second timeout
            )
            response.raise_for_status()
            return response.text
        except requests.RequestException as e:
            raise ScriptExecutionError(f"HTTP GET request failed: {e}")

    def http_post(self, url, data=None, headers=None):
        """
        Make an HTTP POST request to approved domains.

        Args:
            url: URL to post to (must be whitelisted)
            data: Data to send (dict or string)
            headers: Optional headers dictionary

        Returns:
            Response text

        Raises:
            ScriptExecutionError: If URL not whitelisted or request fails
        """
        self._check_api_limit()
        self._validate_url(url, 'POST')

        import requests

        try:
            # Auto-detect JSON content
            if isinstance(data, dict):
                response = requests.post(
                    url,
                    json=data,
                    headers=headers or {},
                    timeout=10,
                )
            else:
                response = requests.post(
                    url,
                    data=data,
                    headers=headers or {},
                    timeout=10,
                )
            response.raise_for_status()
            return response.text
        except requests.RequestException as e:
            raise ScriptExecutionError(f"HTTP POST request failed: {e}")

    def http_put(self, url, data=None, headers=None):
        """
        Make an HTTP PUT request to approved domains.

        Args:
            url: URL to put to (must be whitelisted)
            data: Data to send (dict or string)
            headers: Optional headers dictionary

        Returns:
            Response text

        Raises:
            ScriptExecutionError: If URL not whitelisted or request fails
        """
        self._check_api_limit()
        self._validate_url(url, 'PUT')

        import requests

        try:
            # Auto-detect JSON content
            if isinstance(data, dict):
                response = requests.put(
                    url,
                    json=data,
                    headers=headers or {},
                    timeout=10,
                )
            else:
                response = requests.put(
                    url,
                    data=data,
                    headers=headers or {},
                    timeout=10,
                )
            response.raise_for_status()
            return response.text
        except requests.RequestException as e:
            raise ScriptExecutionError(f"HTTP PUT request failed: {e}")

    def http_patch(self, url, data=None, headers=None):
        """
        Make an HTTP PATCH request to approved domains.

        Args:
            url: URL to patch (must be whitelisted)
            data: Data to send (dict or string)
            headers: Optional headers dictionary

        Returns:
            Response text

        Raises:
            ScriptExecutionError: If URL not whitelisted or request fails
        """
        self._check_api_limit()
        self._validate_url(url, 'PATCH')

        import requests

        try:
            # Auto-detect JSON content
            if isinstance(data, dict):
                response = requests.patch(
                    url,
                    json=data,
                    headers=headers or {},
                    timeout=10,
                )
            else:
                response = requests.patch(
                    url,
                    data=data,
                    headers=headers or {},
                    timeout=10,
                )
            response.raise_for_status()
            return response.text
        except requests.RequestException as e:
            raise ScriptExecutionError(f"HTTP PATCH request failed: {e}")

    def http_delete(self, url, headers=None):
        """
        Make an HTTP DELETE request to approved domains.

        Args:
            url: URL to delete (must be whitelisted)
            headers: Optional headers dictionary

        Returns:
            Response text

        Raises:
            ScriptExecutionError: If URL not whitelisted or request fails
        """
        self._check_api_limit()
        self._validate_url(url, 'DELETE')

        import requests

        try:
            response = requests.delete(
                url,
                headers=headers or {},
                timeout=10,
            )
            response.raise_for_status()
            return response.text
        except requests.RequestException as e:
            raise ScriptExecutionError(f"HTTP DELETE request failed: {e}")

    def create_appointment(self, title, start_time, end_time, **kwargs):
        """
        Create a new appointment.

        Args:
            title: Appointment title
            start_time: Start datetime (ISO format)
            end_time: End datetime (ISO format)
            notes: Optional notes

        Returns:
            Created appointment dictionary
        """
        self._check_api_limit()

        from .models import Event
        from django.utils import timezone
        from datetime import datetime

        # Parse datetimes
        try:
            start = timezone.make_aware(datetime.fromisoformat(start_time.replace('Z', '+00:00')))
            end = timezone.make_aware(datetime.fromisoformat(end_time.replace('Z', '+00:00')))
        except ValueError as e:
            raise ScriptExecutionError(f"Invalid datetime format: {e}")

        # Create event
        event = Event.objects.create(
            title=title,
            start_time=start,
            end_time=end,
            notes=kwargs.get('notes', ''),
            status='SCHEDULED',
            created_by=self.user,
        )

        return {
            'id': event.id,
            'title': event.title,
            'start_time': event.start_time.isoformat(),
            'end_time': event.end_time.isoformat(),
        }

    def count(self, items):
        """Count items in a list"""
        return len(items)

    def sum(self, items):
        """Sum numeric items"""
        return sum(items)

    def filter(self, items, condition):
        """
        Filter items by condition.

        Example:
            customers = api.get_customers()
            active = api.filter(customers, lambda c: c['email'] != '')
        """
        return [item for item in items if condition(item)]

    # =========================================================================
    # FEATURE CHECK HELPER
    # =========================================================================

    def _check_feature(self, feature_code: str, feature_name: str):
        """
        Check if the business has a required feature enabled.

        Args:
            feature_code: The billing feature code (e.g., 'sms_enabled')
            feature_name: Human-readable name for error messages

        Raises:
            ScriptExecutionError: If feature is not available
        """
        if self.business and hasattr(self.business, 'has_feature'):
            if not self.business.has_feature(feature_code):
                raise ScriptExecutionError(
                    f"{feature_name} is not available on your plan. "
                    f"Please upgrade to access this feature."
                )

    # =========================================================================
    # SMS METHODS
    # =========================================================================

    def send_sms(self, to: str, message: str) -> bool:
        """
        Send an SMS message to a phone number.

        Args:
            to: Phone number (E.164 format recommended, e.g., +15551234567)
            message: SMS message content (max 1600 characters)

        Returns:
            True if sent successfully, False otherwise

        Requires: sms_enabled feature
        """
        self._check_api_limit()
        self._check_feature('sms_enabled', 'SMS messaging')

        # Validate phone number (basic check)
        if not to or len(to) < 10:
            raise ScriptExecutionError(f"Invalid phone number: {to}")

        # Normalize phone number
        to = to.strip().replace(' ', '').replace('-', '').replace('(', '').replace(')', '')

        # Message length limit (SMS segment is 160 chars, allow up to 10 segments)
        if len(message) > 1600:
            raise ScriptExecutionError("SMS message too long (max 1600 characters)")

        try:
            # Import Twilio or SMS service
            from smoothschedule.communication.credits.services import SMSService

            sms_service = SMSService(business=self.business)
            result = sms_service.send_sms(to=to, message=message)

            if result.get('success'):
                logger.info(f"[Customer Script] SMS sent to {to[:6]}***")
                return True
            else:
                logger.warning(f"[Customer Script] SMS failed: {result.get('error')}")
                return False

        except ImportError:
            logger.warning("SMS service not available")
            return False
        except Exception as e:
            logger.error(f"Failed to send SMS: {e}")
            return False

    def get_sms_balance(self) -> Dict[str, Any]:
        """
        Get SMS credit balance for this business.

        Returns:
            Dictionary with balance info:
            - credits_remaining: Number of SMS credits left
            - monthly_limit: Monthly SMS limit from plan
            - credits_used_this_month: Credits used this billing period

        Requires: sms_enabled feature
        """
        self._check_api_limit()
        self._check_feature('sms_enabled', 'SMS messaging')

        try:
            from smoothschedule.communication.credits.models import CreditBalance

            balance = CreditBalance.objects.filter(
                business=self.business,
                credit_type='sms'
            ).first()

            # Get plan limit
            monthly_limit = 0
            if self.business and hasattr(self.business, 'get_feature_value'):
                monthly_limit = self.business.get_feature_value('max_sms_per_month') or 0

            return {
                'credits_remaining': balance.balance if balance else 0,
                'monthly_limit': monthly_limit,
                'credits_used_this_month': balance.used_this_period if balance else 0,
            }
        except Exception as e:
            logger.error(f"Failed to get SMS balance: {e}")
            return {'credits_remaining': 0, 'monthly_limit': 0, 'credits_used_this_month': 0}

    # =========================================================================
    # RESOURCE METHODS
    # =========================================================================

    def get_resources(self, **filters) -> List[Dict[str, Any]]:
        """
        Get resources (staff, rooms, equipment) for this business with comprehensive filtering.

        Supported filters:
            - id: Exact resource ID
            - type: Filter by type (STAFF, ROOM, EQUIPMENT)
            - type__in: Multiple types ['STAFF', 'ROOM']
            - name__icontains: Name contains text (case-insensitive)
            - description__icontains: Description contains text
            - is_active: Filter by active status (default: True)
            - is_mobile: Filter by mobile status
            - location_id: Filter by location ID
            - user_id: Filter by linked user ID
            - max_concurrent_events__gte, max_concurrent_events__lte: Filter by concurrency
            - limit: Maximum results (default: 100, max: 500)

        Returns:
            List of resource dictionaries with fields:
            - id, name, type, resource_type_name, description, is_active
            - max_concurrent_events, location_id, location_name
            - is_mobile, user_id, user_name, user_email
        """
        self._check_api_limit()

        from .models import Resource

        queryset = Resource.objects.all()

        # ID filter
        if 'id' in filters:
            queryset = queryset.filter(id=filters['id'])

        # Type filters
        if 'type' in filters:
            queryset = queryset.filter(type=filters['type'])
        if 'type__in' in filters:
            queryset = queryset.filter(type__in=filters['type__in'])

        # Text search filters
        if 'name__icontains' in filters:
            queryset = queryset.filter(name__icontains=filters['name__icontains'])
        if 'description__icontains' in filters:
            queryset = queryset.filter(description__icontains=filters['description__icontains'])

        # Boolean/status filters
        if 'is_active' in filters:
            queryset = queryset.filter(is_active=filters['is_active'])
        elif filters.get('is_active', True):  # Default to True
            queryset = queryset.filter(is_active=True)

        if 'is_mobile' in filters:
            queryset = queryset.filter(is_mobile=filters['is_mobile'])

        # Related object filters
        if 'location_id' in filters:
            queryset = queryset.filter(location_id=filters['location_id'])
        if 'user_id' in filters:
            queryset = queryset.filter(user_id=filters['user_id'])

        # Numeric comparison filters
        for op in ['__gte', '__lte', '__gt', '__lt']:
            key = f'max_concurrent_events{op}'
            if key in filters:
                queryset = queryset.filter(**{key: filters[key]})

        # Enforce limits
        limit = min(filters.get('limit', 100), 500)
        queryset = queryset.select_related('location', 'user', 'resource_type')[:limit]

        return [
            {
                'id': r.id,
                'name': r.name,
                'type': r.type,
                'resource_type_name': r.resource_type.name if r.resource_type else r.type,
                'description': r.description,
                'is_active': r.is_active,
                'max_concurrent_events': r.max_concurrent_events,
                'location_id': r.location_id,
                'location_name': r.location.name if r.location else None,
                'is_mobile': r.is_mobile,
                'user_id': r.user_id,
                'user_name': r.user.get_full_name() if r.user else None,
                'user_email': r.user.email if r.user else None,
            }
            for r in queryset
        ]

    def get_resource_availability(
        self,
        resource_id: int,
        start_date: str = None,
        end_date: str = None,
        days: int = 7
    ) -> Dict[str, Any]:
        """
        Get availability information for a specific resource.

        Args:
            resource_id: ID of the resource
            start_date: Start date (YYYY-MM-DD), defaults to today
            end_date: End date (YYYY-MM-DD), defaults to start_date + days
            days: Number of days to check (default: 7, max: 30)

        Returns:
            Dictionary with:
            - resource_id, resource_name
            - total_slots: Total bookable time slots
            - booked_slots: Number of booked slots
            - available_slots: Number of available slots
            - utilization: Booking percentage (0-100)
            - appointments: List of appointments in the period
        """
        self._check_api_limit()

        from .models import Resource, Event
        from django.utils import timezone
        from datetime import datetime, timedelta

        # Limit days
        days = min(days, 30)

        # Parse dates
        if start_date:
            start = timezone.make_aware(datetime.strptime(start_date, '%Y-%m-%d'))
        else:
            start = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)

        if end_date:
            end = timezone.make_aware(datetime.strptime(end_date, '%Y-%m-%d'))
        else:
            end = start + timedelta(days=days)

        # Get resource
        try:
            resource = Resource.objects.get(id=resource_id)
        except Resource.DoesNotExist:
            raise ScriptExecutionError(f"Resource {resource_id} not found")

        # Get appointments for this resource in the period
        from .models import Participant
        from django.contrib.contenttypes.models import ContentType

        resource_ct = ContentType.objects.get_for_model(Resource)
        participant_event_ids = Participant.objects.filter(
            content_type=resource_ct,
            object_id=resource_id,
            event__start_time__gte=start,
            event__start_time__lt=end
        ).values_list('event_id', flat=True)

        appointments = Event.objects.filter(id__in=participant_event_ids)

        # Calculate utilization (simplified: hours booked / total hours)
        total_hours = days * 8  # Assume 8 working hours per day
        booked_hours = sum(
            (apt.end_time - apt.start_time).total_seconds() / 3600
            for apt in appointments
            if apt.status in ['SCHEDULED', 'COMPLETED', 'PAID']
        )
        utilization = (booked_hours / total_hours * 100) if total_hours > 0 else 0

        return {
            'resource_id': resource.id,
            'resource_name': resource.name,
            'period_start': start.isoformat(),
            'period_end': end.isoformat(),
            'total_hours': total_hours,
            'booked_hours': round(booked_hours, 2),
            'available_hours': round(total_hours - booked_hours, 2),
            'utilization': round(utilization, 1),
            'appointment_count': appointments.count(),
            'appointments': [
                {
                    'id': apt.id,
                    'title': apt.title,
                    'start_time': apt.start_time.isoformat(),
                    'end_time': apt.end_time.isoformat(),
                    'status': apt.status,
                }
                for apt in appointments[:50]  # Limit to 50 appointments
            ]
        }

    # =========================================================================
    # SERVICE METHODS
    # =========================================================================

    def get_services(self, **filters) -> List[Dict[str, Any]]:
        """
        Get services offered by this business with comprehensive filtering.

        Supported filters:
            - id: Exact service ID
            - name__icontains: Name contains text (case-insensitive)
            - description__icontains: Description contains text
            - is_active: Filter by active status (default: True)
            - is_global: Filter by global status
            - variable_pricing: Filter by variable pricing
            - requires_deposit: Filter by deposit requirement
            - location_id: Filter by location ID
            - duration__gte, duration__lte: Filter by duration (minutes)
            - price__gte, price__lte: Filter by price (dollars)
            - price_cents__gte, price_cents__lte: Filter by price (cents)
            - limit: Maximum results (default: 100, max: 500)

        Returns:
            List of service dictionaries with fields:
            - id, name, description, duration, price, price_cents
            - is_active, variable_pricing, requires_deposit
            - deposit_amount_cents, deposit_percent, prep_time, takedown_time, is_global
        """
        self._check_api_limit()

        from .models import Service
        from django.db.models import Q

        queryset = Service.objects.all()

        # ID filter
        if 'id' in filters:
            queryset = queryset.filter(id=filters['id'])

        # Text search filters
        if 'name__icontains' in filters:
            queryset = queryset.filter(name__icontains=filters['name__icontains'])
        if 'description__icontains' in filters:
            queryset = queryset.filter(description__icontains=filters['description__icontains'])

        # Boolean filters
        if 'is_active' in filters:
            queryset = queryset.filter(is_active=filters['is_active'])
        elif filters.get('is_active', True):  # Default to True
            queryset = queryset.filter(is_active=True)

        if 'is_global' in filters:
            queryset = queryset.filter(is_global=filters['is_global'])
        if 'variable_pricing' in filters:
            queryset = queryset.filter(variable_pricing=filters['variable_pricing'])
        if 'requires_deposit' in filters:
            queryset = queryset.filter(requires_deposit=filters['requires_deposit'])

        # Location filter
        if 'location_id' in filters:
            queryset = queryset.filter(
                Q(is_global=True) | Q(locations__id=filters['location_id'])
            ).distinct()

        # Numeric comparison filters
        for field in ['duration', 'price_cents', 'deposit_amount_cents', 'prep_time', 'takedown_time']:
            for op in ['__gte', '__lte', '__gt', '__lt']:
                key = f'{field}{op}'
                if key in filters:
                    queryset = queryset.filter(**{key: filters[key]})

        # Price (dollars) comparison - convert to cents
        for op in ['__gte', '__lte', '__gt', '__lt']:
            key = f'price{op}'
            if key in filters:
                cents = int(filters[key] * 100)
                queryset = queryset.filter(**{f'price_cents{op}': cents})

        # Enforce limits
        limit = min(filters.get('limit', 100), 500)
        queryset = queryset[:limit]

        return [
            {
                'id': s.id,
                'name': s.name,
                'description': s.description,
                'duration': s.duration,
                'price': float(s.price),
                'price_cents': s.price_cents,
                'is_active': s.is_active,
                'variable_pricing': s.variable_pricing,
                'requires_deposit': s.requires_deposit,
                'deposit_amount_cents': s.deposit_amount_cents,
                'deposit_percent': float(s.deposit_percent) if s.deposit_percent else None,
                'prep_time': s.prep_time,
                'takedown_time': s.takedown_time,
                'is_global': s.is_global,
            }
            for s in queryset
        ]

    def get_service_stats(self, service_id: int, days: int = 30) -> Dict[str, Any]:
        """
        Get booking statistics for a specific service.

        Args:
            service_id: ID of the service
            days: Number of days to analyze (default: 30, max: 90)

        Returns:
            Dictionary with:
            - service_id, service_name
            - total_bookings: Total appointments
            - completed_bookings: Completed appointments
            - canceled_bookings: Canceled appointments
            - total_revenue_cents: Total revenue in cents
            - average_rating: Average customer rating (if available)
        """
        self._check_api_limit()

        from .models import Service, Event
        from django.utils import timezone
        from datetime import timedelta

        days = min(days, 90)
        start_date = timezone.now() - timedelta(days=days)

        try:
            service = Service.objects.get(id=service_id)
        except Service.DoesNotExist:
            raise ScriptExecutionError(f"Service {service_id} not found")

        # Get appointments for this service
        appointments = Event.objects.filter(
            service_id=service_id,
            start_time__gte=start_date
        )

        completed = appointments.filter(status__in=['COMPLETED', 'PAID'])
        canceled = appointments.filter(status='CANCELED')

        # Calculate revenue (simplified)
        total_revenue = sum(
            apt.price_cents or service.price_cents
            for apt in completed
        )

        return {
            'service_id': service.id,
            'service_name': service.name,
            'period_days': days,
            'total_bookings': appointments.count(),
            'completed_bookings': completed.count(),
            'canceled_bookings': canceled.count(),
            'completion_rate': round(
                completed.count() / appointments.count() * 100, 1
            ) if appointments.count() > 0 else 0,
            'total_revenue_cents': total_revenue,
            'total_revenue': total_revenue / 100,
            'average_revenue_per_booking': round(
                total_revenue / completed.count() / 100, 2
            ) if completed.count() > 0 else 0,
        }

    # =========================================================================
    # PAYMENT / INVOICE METHODS
    # =========================================================================

    def get_payments(self, **filters) -> List[Dict[str, Any]]:
        """
        Get payment records for this business with comprehensive filtering.

        Supported filters:
            - id: Exact payment ID
            - status: Filter by status (completed, pending, failed, refunded)
            - status__in: Multiple statuses ['completed', 'pending']
            - currency: Filter by currency code
            - customer_id: Filter by customer ID
            - customer_email__icontains: Customer email contains text
            - amount__gte, amount__lte: Filter by amount (dollars)
            - amount_cents__gte, amount_cents__lte: Filter by amount (cents)
            - created_at__gte, created_at__lte: Filter by creation date
            - completed_at__gte, completed_at__lte: Filter by completion date
            - days_back: Get payments from last N days (default: 30, max: 365)
            - limit: Maximum results (default: 100, max: 500)

        Returns:
            List of payment dictionaries

        Requires: payment_processing feature
        """
        self._check_api_limit()
        self._check_feature('payment_processing', 'Payment processing')

        from django.utils import timezone
        from datetime import timedelta
        from dateutil.parser import parse as parse_datetime

        def parse_dt(value):
            if isinstance(value, str):
                try:
                    dt = parse_datetime(value)
                    return dt if timezone.is_aware(dt) else timezone.make_aware(dt)
                except (ValueError, TypeError):
                    return None
            return value

        limit = min(filters.get('limit', 100), 500)

        try:
            from smoothschedule.commerce.payments.models import Payment

            queryset = Payment.objects.filter(business=self.business)

            # ID filter
            if 'id' in filters:
                queryset = queryset.filter(id=filters['id'])

            # Status filters
            if 'status' in filters:
                queryset = queryset.filter(status=filters['status'])
            if 'status__in' in filters:
                queryset = queryset.filter(status__in=filters['status__in'])

            # Currency filter
            if 'currency' in filters:
                queryset = queryset.filter(currency=filters['currency'])

            # Customer filters
            if 'customer_id' in filters:
                queryset = queryset.filter(customer_id=filters['customer_id'])
            if 'customer_email__icontains' in filters:
                queryset = queryset.filter(customer__email__icontains=filters['customer_email__icontains'])

            # Amount filters (cents)
            for op in ['__gte', '__lte', '__gt', '__lt']:
                key = f'amount_cents{op}'
                if key in filters:
                    queryset = queryset.filter(**{key: filters[key]})

            # Amount filters (dollars - convert to cents)
            for op in ['__gte', '__lte', '__gt', '__lt']:
                key = f'amount{op}'
                if key in filters:
                    cents = int(filters[key] * 100)
                    queryset = queryset.filter(**{f'amount_cents{op}': cents})

            # DateTime filters
            for field in ['created_at', 'completed_at']:
                for op in ['__gte', '__lte', '__gt', '__lt']:
                    key = f'{field}{op}'
                    if key in filters:
                        dt = parse_dt(filters[key])
                        if dt:
                            queryset = queryset.filter(**{key: dt})

            # Legacy days_back filter
            if 'days_back' in filters and 'created_at__gte' not in filters:
                days_back = min(filters.get('days_back', 30), 365)
                start_date = timezone.now() - timedelta(days=days_back)
                queryset = queryset.filter(created_at__gte=start_date)

            queryset = queryset.select_related('customer')[:limit]

            return [
                {
                    'id': p.id,
                    'amount_cents': p.amount_cents,
                    'amount': p.amount_cents / 100,
                    'status': p.status,
                    'currency': p.currency,
                    'customer_id': p.customer_id,
                    'customer_email': p.customer.email if p.customer else None,
                    'customer_name': p.customer.get_full_name() if p.customer else None,
                    'description': getattr(p, 'description', ''),
                    'created_at': p.created_at.isoformat(),
                    'completed_at': p.completed_at.isoformat() if hasattr(p, 'completed_at') and p.completed_at else None,
                }
                for p in queryset
            ]
        except ImportError:
            logger.warning("Payment model not available")
            return []

    def get_invoices(self, **filters) -> List[Dict[str, Any]]:
        """
        Get invoices for this business with comprehensive filtering.

        Supported filters:
            - id: Exact invoice ID
            - status: Filter by status (draft, open, paid, void, refunded)
            - status__in: Multiple statuses ['paid', 'open']
            - currency: Filter by currency code
            - plan_name__icontains: Plan name contains text
            - total__gte, total__lte: Filter by total (dollars)
            - total_cents__gte, total_cents__lte: Filter by total (cents)
            - created_at__gte, created_at__lte: Filter by creation date
            - paid_at__gte, paid_at__lte: Filter by payment date
            - period_start__gte, period_start__lte: Filter by period start
            - period_end__gte, period_end__lte: Filter by period end
            - days_back: Get invoices from last N days (default: 90, max: 365)
            - limit: Maximum results (default: 100, max: 500)

        Returns:
            List of invoice dictionaries

        Requires: payment_processing feature
        """
        self._check_api_limit()
        self._check_feature('payment_processing', 'Payment processing')

        from smoothschedule.billing.models import Invoice
        from django.utils import timezone
        from datetime import timedelta
        from dateutil.parser import parse as parse_datetime

        def parse_dt(value):
            if isinstance(value, str):
                try:
                    dt = parse_datetime(value)
                    return dt if timezone.is_aware(dt) else timezone.make_aware(dt)
                except (ValueError, TypeError):
                    return None
            return value

        limit = min(filters.get('limit', 100), 500)
        queryset = Invoice.objects.filter(business=self.business)

        # ID filter
        if 'id' in filters:
            queryset = queryset.filter(id=filters['id'])

        # Status filters
        if 'status' in filters:
            queryset = queryset.filter(status=filters['status'])
        if 'status__in' in filters:
            queryset = queryset.filter(status__in=filters['status__in'])

        # Currency filter
        if 'currency' in filters:
            queryset = queryset.filter(currency=filters['currency'])

        # Text search
        if 'plan_name__icontains' in filters:
            queryset = queryset.filter(plan_name_at_billing__icontains=filters['plan_name__icontains'])

        # Amount filters (cents)
        for op in ['__gte', '__lte', '__gt', '__lt']:
            key = f'total_cents{op}'
            if key in filters:
                queryset = queryset.filter(**{f'total_amount{op}': filters[key]})

        # Amount filters (dollars - convert to cents)
        for op in ['__gte', '__lte', '__gt', '__lt']:
            key = f'total{op}'
            if key in filters:
                cents = int(filters[key] * 100)
                queryset = queryset.filter(**{f'total_amount{op}': cents})

        # DateTime filters
        for field in ['created_at', 'paid_at', 'period_start', 'period_end']:
            for op in ['__gte', '__lte', '__gt', '__lt']:
                key = f'{field}{op}'
                if key in filters:
                    dt = parse_dt(filters[key])
                    if dt:
                        queryset = queryset.filter(**{key: dt})

        # Legacy days_back filter
        if 'days_back' in filters and 'created_at__gte' not in filters:
            days_back = min(filters.get('days_back', 90), 365)
            start_date = timezone.now() - timedelta(days=days_back)
            queryset = queryset.filter(created_at__gte=start_date)

        queryset = queryset[:limit]

        return [
            {
                'id': inv.id,
                'status': inv.status,
                'currency': inv.currency,
                'subtotal_cents': inv.subtotal_amount,
                'subtotal': inv.subtotal_amount / 100,
                'tax_cents': inv.tax_amount,
                'tax': inv.tax_amount / 100,
                'total_cents': inv.total_amount,
                'total': inv.total_amount / 100,
                'period_start': inv.period_start.isoformat(),
                'period_end': inv.period_end.isoformat(),
                'plan_name': inv.plan_name_at_billing,
                'created_at': inv.created_at.isoformat(),
                'paid_at': inv.paid_at.isoformat() if inv.paid_at else None,
            }
            for inv in queryset
        ]

    def get_revenue_stats(self, days: int = 30) -> Dict[str, Any]:
        """
        Get revenue statistics for this business.

        Args:
            days: Number of days to analyze (default: 30, max: 365)

        Returns:
            Dictionary with:
            - total_revenue_cents: Total revenue
            - payment_count: Number of payments
            - average_payment_cents: Average payment amount
            - by_day: Daily breakdown

        Requires: payment_processing feature
        """
        self._check_api_limit()
        self._check_feature('payment_processing', 'Payment processing')

        from django.utils import timezone
        from datetime import timedelta
        from collections import defaultdict

        days = min(days, 365)
        start_date = timezone.now() - timedelta(days=days)

        payments = self.get_payments(days_back=days, status='completed', limit=500)

        # Calculate totals
        total_revenue = sum(p['amount_cents'] for p in payments)
        payment_count = len(payments)

        # Group by day
        by_day = defaultdict(lambda: {'count': 0, 'amount_cents': 0})
        for p in payments:
            day = p['created_at'][:10]  # YYYY-MM-DD
            by_day[day]['count'] += 1
            by_day[day]['amount_cents'] += p['amount_cents']

        return {
            'period_days': days,
            'total_revenue_cents': total_revenue,
            'total_revenue': total_revenue / 100,
            'payment_count': payment_count,
            'average_payment_cents': total_revenue // payment_count if payment_count > 0 else 0,
            'average_payment': round(total_revenue / payment_count / 100, 2) if payment_count > 0 else 0,
            'by_day': dict(by_day),
        }

    # =========================================================================
    # CONTRACT METHODS
    # =========================================================================

    def get_contracts(self, **filters) -> List[Dict[str, Any]]:
        """
        Get contracts for this business with comprehensive filtering.

        Supported filters:
            - id: Exact contract ID
            - status: Filter by status (PENDING, SIGNED, EXPIRED, VOIDED)
            - status__in: Multiple statuses ['PENDING', 'SIGNED']
            - customer_id: Filter by customer ID
            - customer_email__icontains: Customer email contains text
            - title__icontains: Title contains text
            - template_name__icontains: Template name contains text
            - expires_at__gte, expires_at__lte: Filter by expiration date
            - sent_at__gte, sent_at__lte: Filter by sent date
            - created_at__gte, created_at__lte: Filter by creation date
            - limit: Maximum results (default: 100, max: 500)

        Returns:
            List of contract dictionaries

        Requires: can_use_contracts feature
        """
        self._check_api_limit()
        self._check_feature('can_use_contracts', 'Contract management')

        from smoothschedule.scheduling.contracts.models import Contract
        from django.utils import timezone
        from dateutil.parser import parse as parse_datetime

        def parse_dt(value):
            if isinstance(value, str):
                try:
                    dt = parse_datetime(value)
                    return dt if timezone.is_aware(dt) else timezone.make_aware(dt)
                except (ValueError, TypeError):
                    return None
            return value

        limit = min(filters.get('limit', 100), 500)
        queryset = Contract.objects.select_related('customer', 'template')

        # ID filter
        if 'id' in filters:
            queryset = queryset.filter(id=filters['id'])

        # Status filters
        if 'status' in filters:
            queryset = queryset.filter(status=filters['status'])
        if 'status__in' in filters:
            queryset = queryset.filter(status__in=filters['status__in'])

        # Customer filters
        if 'customer_id' in filters:
            queryset = queryset.filter(customer_id=filters['customer_id'])
        if 'customer_email__icontains' in filters:
            queryset = queryset.filter(customer__email__icontains=filters['customer_email__icontains'])

        # Text search filters
        if 'title__icontains' in filters:
            queryset = queryset.filter(title__icontains=filters['title__icontains'])
        if 'template_name__icontains' in filters:
            queryset = queryset.filter(template__name__icontains=filters['template_name__icontains'])

        # DateTime filters
        for field in ['expires_at', 'sent_at', 'created_at']:
            for op in ['__gte', '__lte', '__gt', '__lt']:
                key = f'{field}{op}'
                if key in filters:
                    dt = parse_dt(filters[key])
                    if dt:
                        queryset = queryset.filter(**{key: dt})

        queryset = queryset[:limit]

        return [
            {
                'id': c.id,
                'title': c.title,
                'status': c.status,
                'customer_id': c.customer_id,
                'customer_email': c.customer.email,
                'customer_name': c.customer.get_full_name(),
                'template_name': c.template.name if c.template else None,
                'expires_at': c.expires_at.isoformat() if c.expires_at else None,
                'sent_at': c.sent_at.isoformat() if c.sent_at else None,
                'created_at': c.created_at.isoformat(),
            }
            for c in queryset
        ]

    def get_expiring_contracts(self, days: int = 30) -> List[Dict[str, Any]]:
        """
        Get contracts expiring within the specified number of days.

        Args:
            days: Days until expiration (default: 30, max: 90)

        Returns:
            List of expiring contract dictionaries

        Requires: can_use_contracts feature
        """
        self._check_api_limit()
        self._check_feature('can_use_contracts', 'Contract management')

        from smoothschedule.scheduling.contracts.models import Contract
        from django.utils import timezone
        from datetime import timedelta

        days = min(days, 90)
        now = timezone.now()
        expiry_date = now + timedelta(days=days)

        queryset = Contract.objects.filter(
            status=Contract.Status.SIGNED,
            expires_at__isnull=False,
            expires_at__gte=now,
            expires_at__lte=expiry_date
        ).select_related('customer')[:100]

        return [
            {
                'id': c.id,
                'title': c.title,
                'customer_id': c.customer_id,
                'customer_email': c.customer.email,
                'customer_name': c.customer.get_full_name(),
                'expires_at': c.expires_at.isoformat(),
                'days_until_expiry': (c.expires_at - now).days,
            }
            for c in queryset
        ]

    # =========================================================================
    # LOCATION METHODS
    # =========================================================================

    def get_locations(self, **filters) -> List[Dict[str, Any]]:
        """
        Get business locations with comprehensive filtering.

        Supported filters:
            - id: Exact location ID
            - name__icontains: Name contains text (case-insensitive)
            - city__icontains: City contains text
            - state: Exact state/province match
            - country: Exact country match
            - postal_code: Exact postal code match
            - timezone: Exact timezone match
            - is_active: Filter by active status (default: True)
            - is_primary: Filter by primary status
            - has_phone: True = has phone, False = no phone
            - has_email: True = has email, False = no email
            - limit: Maximum results (default: 50, max: 100)

        Returns:
            List of location dictionaries

        Requires: multi_location feature (returns single location otherwise)
        """
        self._check_api_limit()

        from .models import Location
        from django.db.models import Q

        queryset = Location.objects.filter(business=self.business)

        # ID filter
        if 'id' in filters:
            queryset = queryset.filter(id=filters['id'])

        # Text search filters
        if 'name__icontains' in filters:
            queryset = queryset.filter(name__icontains=filters['name__icontains'])
        if 'city__icontains' in filters:
            queryset = queryset.filter(city__icontains=filters['city__icontains'])

        # Exact match filters
        if 'state' in filters:
            queryset = queryset.filter(state=filters['state'])
        if 'country' in filters:
            queryset = queryset.filter(country=filters['country'])
        if 'postal_code' in filters:
            queryset = queryset.filter(postal_code=filters['postal_code'])
        if 'timezone' in filters:
            queryset = queryset.filter(timezone=filters['timezone'])

        # Boolean filters
        if 'is_active' in filters:
            queryset = queryset.filter(is_active=filters['is_active'])
        elif filters.get('is_active', True):  # Default to True
            queryset = queryset.filter(is_active=True)

        if 'is_primary' in filters:
            queryset = queryset.filter(is_primary=filters['is_primary'])

        # Has phone/email helpers
        if 'has_phone' in filters:
            if filters['has_phone']:
                queryset = queryset.exclude(phone='').exclude(phone__isnull=True)
            else:
                queryset = queryset.filter(Q(phone='') | Q(phone__isnull=True))

        if 'has_email' in filters:
            if filters['has_email']:
                queryset = queryset.exclude(email='').exclude(email__isnull=True)
            else:
                queryset = queryset.filter(Q(email='') | Q(email__isnull=True))

        limit = min(filters.get('limit', 50), 100)
        queryset = queryset[:limit]

        return [
            {
                'id': loc.id,
                'name': loc.name,
                'address_line1': loc.address_line1,
                'address_line2': loc.address_line2,
                'city': loc.city,
                'state': loc.state,
                'postal_code': loc.postal_code,
                'country': loc.country,
                'phone': loc.phone,
                'email': loc.email,
                'timezone': loc.timezone,
                'is_active': loc.is_active,
                'is_primary': loc.is_primary,
            }
            for loc in queryset
        ]

    def get_location_stats(self, location_id: int, days: int = 30) -> Dict[str, Any]:
        """
        Get booking statistics for a specific location.

        Args:
            location_id: ID of the location
            days: Number of days to analyze (default: 30, max: 90)

        Returns:
            Dictionary with booking stats for the location

        Requires: multi_location feature
        """
        self._check_api_limit()
        self._check_feature('multi_location', 'Multi-location support')

        from .models import Location, Event, Resource
        from django.utils import timezone
        from datetime import timedelta

        days = min(days, 90)
        start_date = timezone.now() - timedelta(days=days)

        try:
            location = Location.objects.get(id=location_id, business=self.business)
        except Location.DoesNotExist:
            raise ScriptExecutionError(f"Location {location_id} not found")

        # Get resources at this location
        resource_ids = Resource.objects.filter(
            location=location
        ).values_list('id', flat=True)

        # Get appointments for resources at this location
        from .models import Participant
        from django.contrib.contenttypes.models import ContentType

        resource_ct = ContentType.objects.get_for_model(Resource)
        event_ids = Participant.objects.filter(
            content_type=resource_ct,
            object_id__in=resource_ids,
            event__start_time__gte=start_date
        ).values_list('event_id', flat=True).distinct()

        appointments = Event.objects.filter(id__in=event_ids)
        completed = appointments.filter(status__in=['COMPLETED', 'PAID'])
        canceled = appointments.filter(status='CANCELED')

        return {
            'location_id': location.id,
            'location_name': location.name,
            'period_days': days,
            'resource_count': len(resource_ids),
            'total_bookings': appointments.count(),
            'completed_bookings': completed.count(),
            'canceled_bookings': canceled.count(),
            'completion_rate': round(
                completed.count() / appointments.count() * 100, 1
            ) if appointments.count() > 0 else 0,
        }

    # =========================================================================
    # STAFF METHODS
    # =========================================================================

    def get_staff(self, **filters) -> List[Dict[str, Any]]:
        """
        Get staff members for this business with comprehensive filtering.

        Supported filters:
            - id: Exact staff ID
            - role: Filter by role (staff, manager, owner, resource)
            - role__in: Multiple roles ['staff', 'manager']
            - email: Exact email match
            - email__icontains: Email contains text (case-insensitive)
            - name__icontains: Name contains text (searches first/last/username)
            - first_name__icontains: First name contains text
            - last_name__icontains: Last name contains text
            - is_active: Filter by active status (default: True)
            - has_phone: True = has phone, False = no phone
            - limit: Maximum results (default: 100, max: 500)

        Returns:
            List of staff dictionaries
        """
        self._check_api_limit()

        from smoothschedule.identity.users.models import User
        from django.db.models import Q

        # Staff roles
        staff_roles = ['staff', 'manager', 'owner', 'resource']

        queryset = User.objects.filter(role__in=staff_roles)

        # ID filter
        if 'id' in filters:
            queryset = queryset.filter(id=filters['id'])

        # Role filters
        if 'role' in filters:
            queryset = queryset.filter(role=filters['role'])
        if 'role__in' in filters:
            queryset = queryset.filter(role__in=filters['role__in'])

        # Email filters
        if 'email' in filters:
            queryset = queryset.filter(email=filters['email'])
        if 'email__icontains' in filters:
            queryset = queryset.filter(email__icontains=filters['email__icontains'])

        # Name filters
        if 'name__icontains' in filters:
            search = filters['name__icontains']
            queryset = queryset.filter(
                Q(first_name__icontains=search) |
                Q(last_name__icontains=search) |
                Q(username__icontains=search)
            )
        if 'first_name__icontains' in filters:
            queryset = queryset.filter(first_name__icontains=filters['first_name__icontains'])
        if 'last_name__icontains' in filters:
            queryset = queryset.filter(last_name__icontains=filters['last_name__icontains'])

        # Boolean filters
        if 'is_active' in filters:
            queryset = queryset.filter(is_active=filters['is_active'])
        elif filters.get('is_active', True):  # Default to True
            queryset = queryset.filter(is_active=True)

        if 'has_phone' in filters:
            if filters['has_phone']:
                queryset = queryset.exclude(phone='').exclude(phone__isnull=True)
            else:
                queryset = queryset.filter(Q(phone='') | Q(phone__isnull=True))

        limit = min(filters.get('limit', 100), 500)
        queryset = queryset[:limit]

        return [
            {
                'id': user.id,
                'email': user.email,
                'name': user.get_full_name() or user.username,
                'first_name': user.first_name,
                'last_name': user.last_name,
                'role': user.role,
                'is_active': user.is_active,
                'phone': getattr(user, 'phone', ''),
            }
            for user in queryset
        ]

    def get_staff_performance(self, staff_id: int = None, days: int = 30) -> Dict[str, Any]:
        """
        Get performance statistics for staff members.

        Args:
            staff_id: Specific staff member ID (optional, returns all if not specified)
            days: Number of days to analyze (default: 30, max: 90)

        Returns:
            Dictionary with performance stats
        """
        self._check_api_limit()

        from .models import Resource, Event, Participant
        from smoothschedule.identity.users.models import User
        from django.contrib.contenttypes.models import ContentType
        from django.utils import timezone
        from datetime import timedelta

        days = min(days, 90)
        start_date = timezone.now() - timedelta(days=days)

        # Get staff resources
        queryset = Resource.objects.filter(type='STAFF', is_active=True)
        if staff_id:
            queryset = queryset.filter(user_id=staff_id)

        queryset = queryset.select_related('user')

        resource_ct = ContentType.objects.get_for_model(Resource)
        results = []

        for resource in queryset[:50]:  # Limit to 50 staff
            # Get appointments for this resource
            event_ids = Participant.objects.filter(
                content_type=resource_ct,
                object_id=resource.id,
                event__start_time__gte=start_date
            ).values_list('event_id', flat=True)

            appointments = Event.objects.filter(id__in=event_ids)
            completed = appointments.filter(status__in=['COMPLETED', 'PAID'])
            canceled = appointments.filter(status='CANCELED')
            no_shows = appointments.filter(status='NO_SHOW')

            # Calculate hours worked
            hours_worked = sum(
                (apt.end_time - apt.start_time).total_seconds() / 3600
                for apt in completed
            )

            results.append({
                'staff_id': resource.user_id,
                'staff_name': resource.user.get_full_name() if resource.user else resource.name,
                'resource_id': resource.id,
                'resource_name': resource.name,
                'total_appointments': appointments.count(),
                'completed_appointments': completed.count(),
                'canceled_appointments': canceled.count(),
                'no_show_appointments': no_shows.count(),
                'completion_rate': round(
                    completed.count() / appointments.count() * 100, 1
                ) if appointments.count() > 0 else 0,
                'hours_worked': round(hours_worked, 1),
            })

        if staff_id and len(results) == 1:
            return results[0]

        return {
            'period_days': days,
            'staff_count': len(results),
            'staff': results,
        }

    # =========================================================================
    # VIDEO MEETING METHODS
    # =========================================================================

    def create_video_meeting(
        self,
        provider: str = 'zoom',
        title: str = 'Video Appointment',
        duration: int = 60,
        start_time: str = None
    ) -> Dict[str, Any]:
        """
        Create a video meeting link.

        Args:
            provider: Video provider ('zoom', 'google_meet', 'teams')
            title: Meeting title
            duration: Duration in minutes (default: 60)
            start_time: ISO datetime for scheduled meeting (optional)

        Returns:
            Dictionary with:
            - join_url: URL for participants to join
            - host_url: URL for host (if different)
            - meeting_id: Provider's meeting ID
            - password: Meeting password (if applicable)

        Requires: can_add_video_conferencing feature
        """
        self._check_api_limit()
        self._check_feature('can_add_video_conferencing', 'Video conferencing')

        # Validate provider
        valid_providers = ['zoom', 'google_meet', 'teams']
        if provider not in valid_providers:
            raise ScriptExecutionError(
                f"Invalid video provider '{provider}'. "
                f"Valid options: {', '.join(valid_providers)}"
            )

        try:
            # This would integrate with the actual video service
            # For now, return a placeholder structure
            from django.utils import timezone
            import secrets

            meeting_id = secrets.token_hex(8)

            # In production, this would call Zoom/Google/Teams API
            logger.info(f"[Customer Script] Creating {provider} meeting: {title}")

            return {
                'provider': provider,
                'meeting_id': meeting_id,
                'title': title,
                'duration': duration,
                'join_url': f"https://{provider}.example.com/j/{meeting_id}",
                'host_url': f"https://{provider}.example.com/h/{meeting_id}",
                'password': secrets.token_hex(4),
                'created_at': timezone.now().isoformat(),
            }

        except Exception as e:
            logger.error(f"Failed to create video meeting: {e}")
            raise ScriptExecutionError(f"Failed to create video meeting: {e}")

    # =========================================================================
    # SYSTEM EMAIL TEMPLATE METHODS
    # Plugins can send emails using system-level templates configured in
    # Business Settings > Email Templates.
    # =========================================================================

    def get_system_email_types(self) -> List[Dict[str, str]]:
        """
        Get available system email types that can be sent.

        Returns a list of email types that have templates configured,
        such as appointment confirmations, reminders, etc.

        Returns:
            List of dictionaries with email type info:
            [
                {
                    'type': 'appointment_confirmation',
                    'display_name': 'Appointment Confirmation',
                    'description': 'Sent when appointment is booked',
                    'category': 'appointment'
                },
                ...
            ]
        """
        self._check_api_limit()

        try:
            from smoothschedule.communication.messaging.email_types import (
                EmailType, EMAIL_TYPE_INFO
            )

            return [
                {
                    'type': email_type.value,
                    'display_name': info['display_name'],
                    'description': info['description'],
                    'category': info['category'],
                }
                for email_type, info in EMAIL_TYPE_INFO.items()
            ]
        except ImportError:
            logger.warning("Email types not available")
            return []

    def send_system_email(
        self,
        email_type: str,
        to: str,
        context: Dict[str, str] = None
    ) -> bool:
        """
        Send an email using a system template.

        Args:
            email_type: Type of email to send (e.g., 'appointment_confirmation')
                       Use get_system_email_types() to see available types.
            to: Recipient email address
            context: Dictionary of context variables for template rendering.
                    Common variables include:
                    - CUSTOMER_NAME, CUSTOMER_EMAIL
                    - BUSINESS_NAME, BUSINESS_EMAIL, BUSINESS_PHONE
                    - APPOINTMENT_DATE, APPOINTMENT_TIME
                    - SERVICE_NAME, STAFF_NAME
                    Check template's allowed tags for full list.

        Returns:
            True if email was sent successfully, False otherwise

        Example:
            api.send_system_email(
                email_type='appointment_confirmation',
                to='customer@example.com',
                context={
                    'CUSTOMER_NAME': 'John Doe',
                    'APPOINTMENT_DATE': 'January 15, 2025',
                    'APPOINTMENT_TIME': '2:00 PM',
                    'SERVICE_NAME': 'Consultation',
                }
            )
        """
        self._check_api_limit()

        try:
            from smoothschedule.communication.messaging.models import PuckEmailTemplate
            from smoothschedule.communication.messaging.email_renderer import render_email_template
            from django.core.mail import EmailMultiAlternatives
            from django.conf import settings

            # Find the template for this email type
            template = PuckEmailTemplate.objects.filter(
                email_type=email_type,
                is_active=True
            ).first()

            if not template:
                logger.warning(f"No active template found for email type: {email_type}")
                return False

            # Merge provided context with business context
            full_context = self._get_insertion_context()
            if context:
                full_context.update(context)

            # Render the email
            rendered = render_email_template(template, full_context)

            # Send the email
            from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@smoothschedule.com')

            msg = EmailMultiAlternatives(
                subject=rendered['subject'],
                body=rendered['text'],
                from_email=from_email,
                to=[to],
            )

            if rendered['html']:
                msg.attach_alternative(rendered['html'], 'text/html')

            msg.send(fail_silently=False)

            logger.info(f"[Customer Script] System email '{email_type}' sent to {to}")
            return True

        except Exception as e:
            logger.error(f"Failed to send system email: {e}")
            return False

    # =========================================================================
    # EMAIL TEMPLATE METHODS (DEPRECATED - use system email methods above)
    # =========================================================================

    def get_email_templates(self, **filters) -> List[Dict[str, Any]]:
        """
        DEPRECATED: Custom email templates are no longer supported.
        Use get_system_email_types() instead.

        Returns:
            Empty list (custom templates no longer available)
        """
        logger.warning(
            "[Customer Script] get_email_templates() is deprecated. "
            "Use get_system_email_types() instead."
        )
        return []

    def send_template_email(
        self,
        template_id: int,
        to: str,
        variables: Dict[str, str] = None
    ) -> bool:
        """
        DEPRECATED: Custom email templates are no longer supported.
        Use send_system_email() instead.

        Returns:
            False (custom templates no longer available)
        """
        logger.warning(
            "[Customer Script] send_template_email() is deprecated. "
            "Use send_system_email() instead."
        )
        return False

    # =========================================================================
    # ANALYTICS METHODS
    # =========================================================================

    def get_analytics(self, **filters) -> Dict[str, Any]:
        """
        Get comprehensive analytics for this business.

        Args:
            days: Number of days to analyze (default: 30, max: 90)
            metrics: List of metrics to include (optional)
                - 'bookings', 'revenue', 'customers', 'staff', 'services'

        Returns:
            Dictionary with analytics data

        Requires: advanced_reporting feature
        """
        self._check_api_limit()
        self._check_feature('advanced_reporting', 'Advanced analytics')

        from django.utils import timezone
        from datetime import timedelta

        days = min(filters.get('days', 30), 90)
        start_date = timezone.now() - timedelta(days=days)

        metrics = filters.get('metrics', ['bookings', 'customers', 'services'])

        result = {
            'period_days': days,
            'period_start': start_date.isoformat(),
            'period_end': timezone.now().isoformat(),
        }

        # Bookings analytics
        if 'bookings' in metrics:
            appointments = self.get_appointments(
                start_date=start_date.strftime('%Y-%m-%d'),
                limit=1000
            )
            completed = [a for a in appointments if a['status'] in ['COMPLETED', 'PAID']]
            canceled = [a for a in appointments if a['status'] == 'CANCELED']

            result['bookings'] = {
                'total': len(appointments),
                'completed': len(completed),
                'canceled': len(canceled),
                'completion_rate': round(len(completed) / len(appointments) * 100, 1) if appointments else 0,
            }

        # Customer analytics
        if 'customers' in metrics:
            customers = self.get_customers(limit=1000)
            result['customers'] = {
                'total': len(customers),
                'with_email': len([c for c in customers if c['email']]),
                'with_phone': len([c for c in customers if c.get('phone')]),
            }

        # Service analytics
        if 'services' in metrics:
            services = self.get_services(limit=500)
            result['services'] = {
                'total': len(services),
                'active': len([s for s in services if s['is_active']]),
            }

        # Staff analytics
        if 'staff' in metrics:
            staff = self.get_staff(limit=500)
            result['staff'] = {
                'total': len(staff),
                'active': len([s for s in staff if s['is_active']]),
            }

        return result

    def get_booking_trends(self, days: int = 30, group_by: str = 'day') -> Dict[str, Any]:
        """
        Get booking trends over time.

        Args:
            days: Number of days to analyze (default: 30, max: 90)
            group_by: How to group data ('day', 'week', 'hour', 'weekday')

        Returns:
            Dictionary with trend data

        Requires: advanced_reporting feature
        """
        self._check_api_limit()
        self._check_feature('advanced_reporting', 'Advanced analytics')

        from django.utils import timezone
        from datetime import timedelta
        from collections import defaultdict

        days = min(days, 90)
        start_date = timezone.now() - timedelta(days=days)

        appointments = self.get_appointments(
            start_date=start_date.strftime('%Y-%m-%d'),
            limit=1000
        )

        trends = defaultdict(lambda: {'total': 0, 'completed': 0, 'canceled': 0})

        for apt in appointments:
            # Parse the datetime
            dt_str = apt['start_time']
            from datetime import datetime
            dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00'))

            # Determine group key
            if group_by == 'day':
                key = dt.strftime('%Y-%m-%d')
            elif group_by == 'week':
                key = dt.strftime('%Y-W%W')
            elif group_by == 'hour':
                key = str(dt.hour)
            elif group_by == 'weekday':
                key = dt.strftime('%A')
            else:
                key = dt.strftime('%Y-%m-%d')

            trends[key]['total'] += 1
            if apt['status'] in ['COMPLETED', 'PAID']:
                trends[key]['completed'] += 1
            elif apt['status'] == 'CANCELED':
                trends[key]['canceled'] += 1

        return {
            'period_days': days,
            'group_by': group_by,
            'data': dict(trends),
        }

    # =========================================================================
    # APPOINTMENT UPDATE METHODS
    # =========================================================================

    def update_appointment(self, appointment_id: int, **updates) -> Dict[str, Any]:
        """
        Update an existing appointment.

        Args:
            appointment_id: ID of the appointment to update
            **updates: Fields to update:
                - title: New title
                - notes: New notes
                - status: New status (SCHEDULED, COMPLETED, CANCELED)
                - start_time: New start time (ISO format)
                - end_time: New end time (ISO format)

        Returns:
            Updated appointment dictionary

        Note: Only limited fields can be updated for safety.
        """
        self._check_api_limit()

        from .models import Event
        from django.utils import timezone
        from datetime import datetime

        try:
            event = Event.objects.get(id=appointment_id)
        except Event.DoesNotExist:
            raise ScriptExecutionError(f"Appointment {appointment_id} not found")

        # Allowed update fields
        allowed_fields = ['title', 'notes', 'status']

        for field, value in updates.items():
            if field in allowed_fields:
                setattr(event, field, value)
            elif field == 'start_time':
                event.start_time = timezone.make_aware(
                    datetime.fromisoformat(value.replace('Z', '+00:00'))
                )
            elif field == 'end_time':
                event.end_time = timezone.make_aware(
                    datetime.fromisoformat(value.replace('Z', '+00:00'))
                )
            else:
                logger.warning(f"[Customer Script] Ignoring update to protected field: {field}")

        event.save()

        return {
            'id': event.id,
            'title': event.title,
            'start_time': event.start_time.isoformat(),
            'end_time': event.end_time.isoformat(),
            'status': event.status,
            'notes': event.notes,
        }

    def get_recurring_appointments(self, **filters) -> List[Dict[str, Any]]:
        """
        Get recurring appointment series with comprehensive filtering.

        Supported filters:
            - id: Exact appointment ID
            - status: Filter by status (SCHEDULED, COMPLETED, CANCELED, etc.)
            - status__in: Multiple statuses ['SCHEDULED', 'COMPLETED']
            - title__icontains: Title contains text (case-insensitive)
            - recurring_pattern__icontains: Pattern contains text
            - start_time__gte, start_time__lte: Filter by start time
            - is_active: Filter by active status (default: True)
            - limit: Maximum results (default: 50, max: 200)

        Returns:
            List of recurring series dictionaries

        Requires: recurring_appointments feature
        """
        self._check_api_limit()
        self._check_feature('recurring_appointments', 'Recurring appointments')

        from .models import Event
        from django.utils import timezone
        from dateutil.parser import parse as parse_datetime

        def parse_dt(value):
            if isinstance(value, str):
                try:
                    dt = parse_datetime(value)
                    return dt if timezone.is_aware(dt) else timezone.make_aware(dt)
                except (ValueError, TypeError):
                    return None
            return value

        # Get events that are part of a recurring series
        queryset = Event.objects.filter(
            recurring_pattern__isnull=False
        ).exclude(recurring_pattern='')

        # ID filter
        if 'id' in filters:
            queryset = queryset.filter(id=filters['id'])

        # Status filters
        if 'status' in filters:
            queryset = queryset.filter(status=filters['status'])
        elif 'status__in' in filters:
            queryset = queryset.filter(status__in=filters['status__in'])
        elif filters.get('is_active', True):  # Default to active only
            queryset = queryset.filter(status='SCHEDULED')

        # Text search filters
        if 'title__icontains' in filters:
            queryset = queryset.filter(title__icontains=filters['title__icontains'])
        if 'recurring_pattern__icontains' in filters:
            queryset = queryset.filter(recurring_pattern__icontains=filters['recurring_pattern__icontains'])

        # DateTime filters
        for op in ['__gte', '__lte', '__gt', '__lt']:
            key = f'start_time{op}'
            if key in filters:
                dt = parse_dt(filters[key])
                if dt:
                    queryset = queryset.filter(**{key: dt})

        limit = min(filters.get('limit', 50), 200)

        # Group by recurring pattern/parent
        seen_patterns = set()
        results = []

        for event in queryset[:limit * 2]:  # Get more to account for duplicates
            pattern = event.recurring_pattern
            if pattern not in seen_patterns:
                seen_patterns.add(pattern)
                results.append({
                    'id': event.id,
                    'title': event.title,
                    'recurring_pattern': pattern,
                    'start_time': event.start_time.isoformat(),
                    'status': event.status,
                })

                if len(results) >= limit:
                    break

        return results


class SafeScriptEngine:
    """
    Execute customer scripts safely with resource limits.
    """

    # Resource limits
    MAX_EXECUTION_TIME = 30  # seconds
    MAX_OUTPUT_SIZE = 10000  # characters
    MAX_ITERATIONS = 10000  # loop iterations
    MAX_MEMORY_MB = 50  # megabytes

    # Allowed built-in functions (whitelist)
    SAFE_BUILTINS = {
        'len': len,
        'range': range,
        'min': min,
        'max': max,
        'sum': sum,
        'abs': abs,
        'round': round,
        'int': int,
        'float': float,
        'str': str,
        'bool': bool,
        'list': list,
        'dict': dict,
        'enumerate': enumerate,
        'zip': zip,
        'sorted': sorted,
        'reversed': reversed,
        'any': any,
        'all': all,
        'True': True,
        'False': False,
        'None': None,
    }

    def __init__(self):
        self._iteration_count = 0

    def _check_iterations(self):
        """Track loop iterations to prevent infinite loops"""
        self._iteration_count += 1
        if self._iteration_count > self.MAX_ITERATIONS:
            raise ResourceLimitExceeded(
                f"Loop iteration limit exceeded ({self.MAX_ITERATIONS} iterations)"
            )

    def _validate_script(self, script: str) -> None:
        """
        Validate script before execution.

        Checks for:
        - Forbidden operations (import, exec, eval, etc.)
        - Syntax errors
        - Excessive complexity
        """
        try:
            tree = ast.parse(script)
        except SyntaxError as e:
            raise ScriptExecutionError(f"Syntax error: {e}")

        # Check for forbidden operations
        for node in ast.walk(tree):
            # No imports
            if isinstance(node, (ast.Import, ast.ImportFrom)):
                raise ScriptExecutionError(
                    "Import statements not allowed. Use provided 'api' object instead."
                )

            # No exec/eval/compile
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name):
                    if node.func.id in ['exec', 'eval', 'compile', '__import__']:
                        raise ScriptExecutionError(
                            f"Function '{node.func.id}' not allowed"
                        )

            # No class definitions (for now)
            if isinstance(node, ast.ClassDef):
                raise ScriptExecutionError("Class definitions not allowed")

            # No function definitions (for now - could allow later)
            if isinstance(node, ast.FunctionDef):
                raise ScriptExecutionError(
                    "Function definitions not allowed. Use inline logic instead."
                )

        # Check script size
        if len(script) > 50000:  # 50KB limit
            raise ScriptExecutionError("Script too large (max 50KB)")

    def execute(
        self,
        script: str,
        api: SafeScriptAPI,
        initial_vars: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        """
        Execute a customer script safely.

        Args:
            script: Python code to execute
            api: SafeScriptAPI instance
            initial_vars: Optional initial variables

        Returns:
            Dictionary with execution results:
                - success: bool
                - output: str (captured print statements)
                - result: Any (value of 'result' variable if set)
                - error: str (if failed)
        """
        # Validate script
        self._validate_script(script)

        # Reset iteration counter
        self._iteration_count = 0

        # Prepare safe globals
        safe_globals = {
            '__builtins__': self.SAFE_BUILTINS,
            'api': api,
            '_iteration_check': self._check_iterations,
        }

        # Add initial variables
        if initial_vars:
            safe_globals.update(initial_vars)

        # Inject iteration checks into loops
        script = self._inject_loop_guards(script)

        # Capture stdout/stderr
        stdout_capture = StringIO()
        stderr_capture = StringIO()

        # Execute with timeout
        start_time = time.time()

        try:
            with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
                # Compile and execute
                compiled = compile(script, '<customer_script>', 'exec')
                exec(compiled, safe_globals)

                # Check execution time
                if time.time() - start_time > self.MAX_EXECUTION_TIME:
                    raise ResourceLimitExceeded(
                        f"Execution time exceeded ({self.MAX_EXECUTION_TIME}s)"
                    )

            # Get output
            output = stdout_capture.getvalue()
            if len(output) > self.MAX_OUTPUT_SIZE:
                output = output[:self.MAX_OUTPUT_SIZE] + "\n... (output truncated)"

            # Get result variable if set
            result = safe_globals.get('result', None)

            return {
                'success': True,
                'output': output,
                'result': result,
                'error': None,
                'iterations': self._iteration_count,
                'execution_time': time.time() - start_time,
            }

        except ResourceLimitExceeded as e:
            return {
                'success': False,
                'output': stdout_capture.getvalue(),
                'result': None,
                'error': str(e),
            }

        except Exception as e:
            error_msg = f"{type(e).__name__}: {str(e)}"
            stderr_output = stderr_capture.getvalue()
            if stderr_output:
                error_msg += f"\n{stderr_output}"

            return {
                'success': False,
                'output': stdout_capture.getvalue(),
                'result': None,
                'error': error_msg,
            }

    def _inject_loop_guards(self, script: str) -> str:
        """
        Inject iteration checks into loops to prevent infinite loops.

        Transforms:
            for i in range(10):
                print(i)

        Into:
            for i in range(10):
                _iteration_check()
                print(i)
        """
        try:
            tree = ast.parse(script)
        except SyntaxError:
            # If it doesn't parse, validation will catch it
            return script

        class LoopGuardInjector(ast.NodeTransformer):
            def visit_For(self, node):
                # Add iteration check at start of loop body
                check_call = ast.Expr(
                    value=ast.Call(
                        func=ast.Name(id='_iteration_check', ctx=ast.Load()),
                        args=[],
                        keywords=[]
                    )
                )
                node.body.insert(0, check_call)
                return self.generic_visit(node)

            def visit_While(self, node):
                # Add iteration check at start of loop body
                check_call = ast.Expr(
                    value=ast.Call(
                        func=ast.Name(id='_iteration_check', ctx=ast.Load()),
                        args=[],
                        keywords=[]
                    )
                )
                node.body.insert(0, check_call)
                return self.generic_visit(node)

        transformed = LoopGuardInjector().visit(tree)
        ast.fix_missing_locations(transformed)

        return ast.unparse(transformed)


def analyze_plugin_http_calls(script: str) -> List[Dict[str, Any]]:
    """
    Analyze plugin code to detect HTTP method calls.

    Args:
        script: Plugin code to analyze

    Returns:
        List of dictionaries with detected HTTP calls:
        [
            {
                'method': 'GET',  # HTTP method
                'url': 'https://api.example.com/data',  # URL if detectable
                'line': 5,  # Line number
            },
            ...
        ]

    Raises:
        SyntaxError: If code doesn't parse
    """
    http_calls = []

    try:
        tree = ast.parse(script)
    except SyntaxError as e:
        raise SyntaxError(f"Invalid Python syntax: {e}")

    # HTTP methods to detect
    http_methods = {
        'http_get': 'GET',
        'http_post': 'POST',
        'http_put': 'PUT',
        'http_patch': 'PATCH',
        'http_delete': 'DELETE',
    }

    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            # Check if it's an API call like api.http_get(...)
            if isinstance(node.func, ast.Attribute):
                method_name = node.func.attr
                if method_name in http_methods:
                    http_method = http_methods[method_name]

                    # Try to extract the URL argument (first positional argument)
                    url = None
                    if node.args and len(node.args) > 0:
                        first_arg = node.args[0]
                        # If it's a string literal, extract it
                        if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
                            url = first_arg.value
                        # If it's an f-string or JoinedStr
                        elif isinstance(first_arg, ast.JoinedStr):
                            # Can't statically determine f-string URLs
                            url = '<dynamic URL - f-string>'
                        # If it's a Name (variable)
                        elif isinstance(first_arg, ast.Name):
                            url = f'<variable: {first_arg.id}>'
                        else:
                            url = '<dynamic URL>'

                    http_calls.append({
                        'method': http_method,
                        'url': url,
                        'line': node.lineno,
                    })

    return http_calls


def validate_plugin_whitelist(script: str, scheduled_task=None) -> Dict[str, Any]:
    """
    Validate that all HTTP calls in plugin code are whitelisted.

    Args:
        script: Plugin code to validate
        scheduled_task: ScheduledTask instance (optional, for plugin-specific whitelist)

    Returns:
        Dictionary with validation results:
        {
            'valid': bool,
            'errors': List[str],  # Validation error messages
            'warnings': List[str],  # Warnings (e.g., dynamic URLs)
            'http_calls': List[Dict],  # Detected HTTP calls
        }
    """
    from .models import WhitelistedURL

    result = {
        'valid': True,
        'errors': [],
        'warnings': [],
        'http_calls': [],
    }

    try:
        http_calls = analyze_plugin_http_calls(script)
        result['http_calls'] = http_calls

        for call in http_calls:
            url = call['url']
            method = call['method']
            line = call['line']

            # Skip if URL is dynamic (can't validate statically)
            if not url or url.startswith('<'):
                result['warnings'].append(
                    f"Line {line}: {method} request uses dynamic URL - cannot validate at upload time. "
                    f"Ensure URL is whitelisted before running."
                )
                continue

            # Check if URL is whitelisted
            if not WhitelistedURL.is_url_whitelisted(url, method, scheduled_task):
                result['valid'] = False
                result['errors'].append(
                    f"Line {line}: {method} request to '{url}' is not whitelisted. "
                    f"Contact support at pluginaccess@smoothschedule.com to request whitelisting."
                )

    except SyntaxError as e:
        result['valid'] = False
        result['errors'].append(f"Syntax error: {str(e)}")

    return result


def test_script_execution():
    """Test the safe script engine"""

    engine = SafeScriptEngine()

    # Create mock API
    class MockBusiness:
        name = "Test Business"

    api = SafeScriptAPI(
        business=MockBusiness(),
        user=None,
        execution_context={}
    )

    # Test 1: Simple script
    script1 = """
# Get appointments
appointments = api.get_appointments(status='SCHEDULED', limit=10)

# Count them
count = len(appointments)

# Log result
api.log(f"Found {count} appointments")

result = count
"""

    print("Test 1: Simple script")
    result1 = engine.execute(script1, api)
    print(f"Success: {result1['success']}")
    print(f"Result: {result1['result']}")
    print(f"Output: {result1['output']}")
    print()

    # Test 2: Conditional logic
    script2 = """
appointments = api.get_appointments(limit=100)

# Count by status
scheduled = 0
completed = 0

for apt in appointments:
    if apt['status'] == 'SCHEDULED':
        scheduled += 1
    elif apt['status'] == 'COMPLETED':
        completed += 1

result = {
    'scheduled': scheduled,
    'completed': completed,
    'total': len(appointments)
}
"""

    print("Test 2: Conditional logic")
    result2 = engine.execute(script2, api)
    print(f"Success: {result2['success']}")
    print(f"Result: {result2['result']}")
    print()

    # Test 3: Forbidden operation (should fail)
    script3 = """
import os
os.system('echo hello')
"""

    print("Test 3: Forbidden operation")
    result3 = engine.execute(script3, api)
    print(f"Success: {result3['success']}")
    print(f"Error: {result3['error']}")
    print()

    # Test 4: Infinite loop protection
    script4 = """
count = 0
while True:
    count += 1
"""

    print("Test 4: Infinite loop protection")
    result4 = engine.execute(script4, api)
    print(f"Success: {result4['success']}")
    print(f"Error: {result4['error']}")
    print()


if __name__ == '__main__':
    test_script_execution()
