"""Core Utilities

This module contains utility functions, helpers, and common operations
used across the entire Adtlas project.
"""

import os
import re
import json
import uuid
import hashlib
import secrets
import mimetypes
from urllib.parse import urlparse
from datetime import datetime, timedelta 
from typing import Any, Dict, List, Optional

from django.conf import settings 
from django.utils import timezone
from django.utils.text import slugify
from django.core.mail import send_mail
from django.utils.html import strip_tags
from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _
from django.http import HttpRequest, JsonResponse
from django.template.loader import render_to_string
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger


User = get_user_model()


# ============================================================================
# String and Text Utilities
# ============================================================================

def generate_unique_id(prefix: str = '', length: int = 8) -> str:
    """Generate a unique identifier with optional prefix.
    
    Args:
        prefix: Optional prefix for the ID
        length: Length of the random part
    
    Returns:
        Unique identifier string
    """
    random_part = secrets.token_urlsafe(length)[:length]
    return f"{prefix}{random_part}" if prefix else random_part


def generate_uuid() -> str:
    """Generate a UUID4 string.
    
    Returns:
        UUID4 string
    """
    return str(uuid.uuid4())


def slugify_unique(text: str, model_class, field_name: str = 'slug', 
                  instance=None) -> str:
    """Generate a unique slug for a model field.
    
    Args:
        text: Text to slugify
        model_class: Model class to check uniqueness against
        field_name: Field name to check uniqueness
        instance: Current instance (for updates)
    
    Returns:
        Unique slug string
    """
    base_slug = slugify(text)
    slug = base_slug
    counter = 1
    
    while True:
        # Check if slug exists
        queryset = model_class.objects.filter(**{field_name: slug})
        
        # Exclude current instance if updating
        if instance and instance.pk:
            queryset = queryset.exclude(pk=instance.pk)
        
        if not queryset.exists():
            break
        
        # Generate new slug with counter
        slug = f"{base_slug}-{counter}"
        counter += 1
    
    return slug


def truncate_text(text: str, max_length: int = 100, suffix: str = '...') -> str:
    """Truncate text to specified length with suffix.
    
    Args:
        text: Text to truncate
        max_length: Maximum length
        suffix: Suffix to add if truncated
    
    Returns:
        Truncated text
    """
    if len(text) <= max_length:
        return text
    
    return text[:max_length - len(suffix)] + suffix


def clean_html(html_content: str) -> str:
    """Clean HTML content by removing tags.
    
    Args:
        html_content: HTML content to clean
    
    Returns:
        Clean text without HTML tags
    """
    return strip_tags(html_content).strip()


def extract_keywords(text: str, min_length: int = 3) -> List[str]:
    """Extract keywords from text.
    
    Args:
        text: Text to extract keywords from
        min_length: Minimum keyword length
    
    Returns:
        List of keywords
    """
    # Remove HTML tags and clean text
    clean_text = clean_html(text).lower()
    
    # Extract words using regex
    words = re.findall(r'\b\w+\b', clean_text)
    
    # Filter by length and remove duplicates
    keywords = list(set(word for word in words if len(word) >= min_length))
    
    return sorted(keywords)


# ============================================================================
# Date and Time Utilities
# ============================================================================

def format_datetime(dt: datetime, format_string: str = None) -> str:
    """Format datetime with default or custom format.
    
    Args:
        dt: Datetime to format
        format_string: Custom format string
    
    Returns:
        Formatted datetime string
    """
    if not dt:
        return ''
    
    if format_string:
        return dt.strftime(format_string)
    
    return dt.strftime('%Y-%m-%d %H:%M:%S')


def parse_datetime(date_string: str) -> Optional[datetime]:
    """Parse datetime string with multiple format attempts.
    
    Args:
        date_string: Date string to parse
    
    Returns:
        Parsed datetime or None
    """
    formats = [
        '%Y-%m-%d %H:%M:%S',
        '%Y-%m-%d %H:%M',
        '%Y-%m-%d',
        '%d/%m/%Y %H:%M:%S',
        '%d/%m/%Y %H:%M',
        '%d/%m/%Y',
        '%m/%d/%Y %H:%M:%S',
        '%m/%d/%Y %H:%M',
        '%m/%d/%Y',
    ]
    
    for fmt in formats:
        try:
            return datetime.strptime(date_string, fmt)
        except ValueError:
            continue
    
    return None


def get_date_range(period: str) -> tuple:
    """Get date range for common periods.
    
    Args:
        period: Period name (today, yesterday, this_week, last_week, etc.)
    
    Returns:
        Tuple of (start_date, end_date)
    """
    now = timezone.now()
    today = now.date()
    
    if period == 'today':
        return today, today
    elif period == 'yesterday':
        yesterday = today - timedelta(days=1)
        return yesterday, yesterday
    elif period == 'this_week':
        start = today - timedelta(days=today.weekday())
        return start, today
    elif period == 'last_week':
        start = today - timedelta(days=today.weekday() + 7)
        end = start + timedelta(days=6)
        return start, end
    elif period == 'this_month':
        start = today.replace(day=1)
        return start, today
    elif period == 'last_month':
        first_this_month = today.replace(day=1)
        last_month_end = first_this_month - timedelta(days=1)
        last_month_start = last_month_end.replace(day=1)
        return last_month_start, last_month_end
    elif period == 'this_year':
        start = today.replace(month=1, day=1)
        return start, today
    elif period == 'last_year':
        last_year = today.year - 1
        start = today.replace(year=last_year, month=1, day=1)
        end = today.replace(year=last_year, month=12, day=31)
        return start, end
    
    # Default to today
    return today, today


def time_ago(dt: datetime) -> str:
    """Get human-readable time ago string.
    
    Args:
        dt: Datetime to compare
    
    Returns:
        Human-readable time ago string
    """
    if not dt:
        return ''
    
    now = timezone.now()
    diff = now - dt
    
    if diff.days > 365:
        years = diff.days // 365
        return f"{years} year{'s' if years > 1 else ''} ago"
    elif diff.days > 30:
        months = diff.days // 30
        return f"{months} month{'s' if months > 1 else ''} ago"
    elif diff.days > 0:
        return f"{diff.days} day{'s' if diff.days > 1 else ''} ago"
    elif diff.seconds > 3600:
        hours = diff.seconds // 3600
        return f"{hours} hour{'s' if hours > 1 else ''} ago"
    elif diff.seconds > 60:
        minutes = diff.seconds // 60
        return f"{minutes} minute{'s' if minutes > 1 else ''} ago"
    else:
        return "Just now"


# ============================================================================
# File and Media Utilities
# ============================================================================

def get_file_extension(filename: str) -> str:
    """Get file extension from filename.
    
    Args:
        filename: Name of the file
    
    Returns:
        File extension (without dot)
    """
    return os.path.splitext(filename)[1][1:].lower()


def get_mime_type(filename: str) -> str:
    """Get MIME type for a file.
    
    Args:
        filename: Name of the file
    
    Returns:
        MIME type string
    """
    mime_type, _ = mimetypes.guess_type(filename)
    return mime_type or 'application/octet-stream'


def validate_file_size(file, max_size_mb: int = 10) -> bool:
    """Validate file size.
    
    Args:
        file: File object
        max_size_mb: Maximum size in MB
    
    Returns:
        True if valid, False otherwise
    """
    max_size_bytes = max_size_mb * 1024 * 1024
    return file.size <= max_size_bytes


def validate_file_type(file, allowed_types: List[str]) -> bool:
    """Validate file type.
    
    Args:
        file: File object
        allowed_types: List of allowed MIME types
    
    Returns:
        True if valid, False otherwise
    """
    file_type = get_mime_type(file.name)
    return file_type in allowed_types


def generate_upload_path(instance, filename: str, folder: str = 'uploads') -> str:
    """Generate upload path for files.
    
    Args:
        instance: Model instance
        filename: Original filename
        folder: Upload folder
    
    Returns:
        Upload path
    """
    # Get file extension
    ext = get_file_extension(filename)
    
    # Generate unique filename
    unique_filename = f"{generate_uuid()}.{ext}"
    
    # Create path with date structure
    date_path = timezone.now().strftime('%Y/%m/%d')
    
    return f"{folder}/{date_path}/{unique_filename}"


# ============================================================================
# Data Processing Utilities
# ============================================================================

def safe_json_loads(json_string: str, default=None) -> Any:
    """Safely load JSON string.
    
    Args:
        json_string: JSON string to parse
        default: Default value if parsing fails
    
    Returns:
        Parsed JSON data or default value
    """
    try:
        return json.loads(json_string)
    except (json.JSONDecodeError, TypeError):
        return default


def safe_json_dumps(data: Any, default=None) -> str:
    """Safely dump data to JSON string.
    
    Args:
        data: Data to serialize
        default: Default value if serialization fails
    
    Returns:
        JSON string or default value
    """
    try:
        return json.dumps(data, default=str)
    except (TypeError, ValueError):
        return default or '{}'


def clean_dict(data: Dict, remove_empty: bool = True) -> Dict:
    """Clean dictionary by removing None values and optionally empty values.
    
    Args:
        data: Dictionary to clean
        remove_empty: Whether to remove empty values
    
    Returns:
        Cleaned dictionary
    """
    cleaned = {}
    
    for key, value in data.items():
        if value is None:
            continue
        
        if remove_empty and not value and value != 0 and value is not False:
            continue
        
        cleaned[key] = value
    
    return cleaned


def merge_dicts(*dicts: Dict) -> Dict:
    """Merge multiple dictionaries.
    
    Args:
        *dicts: Dictionaries to merge
    
    Returns:
        Merged dictionary
    """
    result = {}
    
    for d in dicts:
        if isinstance(d, dict):
            result.update(d)
    
    return result


def flatten_dict(data: Dict, separator: str = '.', prefix: str = '') -> Dict:
    """Flatten nested dictionary.
    
    Args:
        data: Dictionary to flatten
        separator: Key separator
        prefix: Key prefix
    
    Returns:
        Flattened dictionary
    """
    result = {}
    
    for key, value in data.items():
        new_key = f"{prefix}{separator}{key}" if prefix else key
        
        if isinstance(value, dict):
            result.update(flatten_dict(value, separator, new_key))
        else:
            result[new_key] = value
    
    return result


# ============================================================================
# Validation Utilities
# ============================================================================

def validate_email(email: str) -> bool:
    """Validate email address format.
    
    Args:
        email: Email address to validate
    
    Returns:
        True if valid, False otherwise
    """
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))


def validate_phone(phone: str) -> bool:
    """Validate phone number format.
    
    Args:
        phone: Phone number to validate
    
    Returns:
        True if valid, False otherwise
    """
    # Remove all non-digit characters
    digits_only = re.sub(r'\D', '', phone)
    
    # Check if it's a valid length (7-15 digits)
    return 7 <= len(digits_only) <= 15


def validate_url(url: str) -> bool:
    """Validate URL format.
    
    Args:
        url: URL to validate
    
    Returns:
        True if valid, False otherwise
    """
    try:
        result = urlparse(url)
        return all([result.scheme, result.netloc])
    except Exception:
        return False


def validate_password_strength(password: str) -> Dict[str, Any]:
    """Validate password strength.
    
    Args:
        password: Password to validate
    
    Returns:
        Dictionary with validation results
    """
    result = {
        'is_valid': True,
        'errors': [],
        'score': 0,
        'strength': 'weak'
    }
    
    # Length check
    if len(password) < 8:
        result['errors'].append('Password must be at least 8 characters long')
        result['is_valid'] = False
    else:
        result['score'] += 1
    
    # Character type checks
    if not re.search(r'[a-z]', password):
        result['errors'].append('Password must contain at least one lowercase letter')
        result['is_valid'] = False
    else:
        result['score'] += 1
    
    if not re.search(r'[A-Z]', password):
        result['errors'].append('Password must contain at least one uppercase letter')
        result['is_valid'] = False
    else:
        result['score'] += 1
    
    if not re.search(r'\d', password):
        result['errors'].append('Password must contain at least one number')
        result['is_valid'] = False
    else:
        result['score'] += 1
    
    if not re.search(r'[!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]', password):
        result['errors'].append('Password must contain at least one special character')
        result['is_valid'] = False
    else:
        result['score'] += 1
    
    # Determine strength
    if result['score'] >= 5:
        result['strength'] = 'very_strong'
    elif result['score'] >= 4:
        result['strength'] = 'strong'
    elif result['score'] >= 3:
        result['strength'] = 'medium'
    elif result['score'] >= 2:
        result['strength'] = 'weak'
    else:
        result['strength'] = 'very_weak'
    
    return result


# ============================================================================
# HTTP and Request Utilities
# ============================================================================

def get_client_ip(request: HttpRequest) -> str:
    """Get client IP address from request.
    
    Args:
        request: HTTP request object
    
    Returns:
        Client IP address
    """
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    return ip


def get_user_agent(request: HttpRequest) -> str:
    """Get user agent from request.
    
    Args:
        request: HTTP request object
    
    Returns:
        User agent string
    """
    return request.META.get('HTTP_USER_AGENT', '')


def is_ajax_request(request: HttpRequest) -> bool:
    """Check if request is AJAX.
    
    Args:
        request: HTTP request object
    
    Returns:
        True if AJAX request, False otherwise
    """
    return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'


def build_absolute_uri(request: HttpRequest, path: str) -> str:
    """Build absolute URI from request and path.
    
    Args:
        request: HTTP request object
        path: Relative path
    
    Returns:
        Absolute URI
    """
    return request.build_absolute_uri(path)


def safe_redirect_url(url: str, allowed_hosts: List[str] = None) -> str:
    """Validate and return safe redirect URL.
    
    Args:
        url: URL to validate
        allowed_hosts: List of allowed hosts
    
    Returns:
        Safe URL or default URL
    """
    if not url:
        return '/'
    
    # Parse URL
    parsed = urlparse(url)
    
    # If no host, it's a relative URL (safe)
    if not parsed.netloc:
        return url
    
    # Check against allowed hosts
    allowed_hosts = allowed_hosts or getattr(settings, 'ALLOWED_HOSTS', [])
    
    if parsed.netloc in allowed_hosts:
        return url
    
    # Default to root
    return '/'


# ============================================================================
# Pagination Utilities
# ============================================================================

def paginate_queryset(queryset, page: int, per_page: int = 25) -> Dict:
    """Paginate queryset and return pagination data.
    
    Args:
        queryset: Django queryset
        page: Page number
        per_page: Items per page
    
    Returns:
        Dictionary with pagination data
    """
    paginator = Paginator(queryset, per_page)
    
    try:
        page_obj = paginator.page(page)
    except PageNotAnInteger:
        page_obj = paginator.page(1)
    except EmptyPage:
        page_obj = paginator.page(paginator.num_pages)
    
    return {
        'objects': page_obj.object_list,
        'page_obj': page_obj,
        'paginator': paginator,
        'has_previous': page_obj.has_previous(),
        'has_next': page_obj.has_next(),
        'previous_page_number': page_obj.previous_page_number() if page_obj.has_previous() else None,
        'next_page_number': page_obj.next_page_number() if page_obj.has_next() else None,
        'current_page': page_obj.number,
        'total_pages': paginator.num_pages,
        'total_count': paginator.count,
        'start_index': page_obj.start_index(),
        'end_index': page_obj.end_index(),
    }


# ============================================================================
# Email Utilities
# ============================================================================

def send_notification_email(to_email: str, subject: str, template_name: str, 
                          context: Dict = None, from_email: str = None) -> bool:
    """Send notification email using template.
    
    Args:
        to_email: Recipient email
        subject: Email subject
        template_name: Template name
        context: Template context
        from_email: Sender email
    
    Returns:
        True if sent successfully, False otherwise
    """
    try:
        context = context or {}
        from_email = from_email or settings.DEFAULT_FROM_EMAIL
        
        # Render email content
        html_content = render_to_string(template_name, context)
        text_content = strip_tags(html_content)
        
        # Send email
        send_mail(
            subject=subject,
            message=text_content,
            from_email=from_email,
            recipient_list=[to_email],
            html_message=html_content,
            fail_silently=False
        )
        
        return True
    
    except Exception as e:
        # Log error (you might want to use proper logging here)
        print(f"Email sending failed: {e}")
        return False


# ============================================================================
# Security Utilities
# ============================================================================

def generate_token(length: int = 32) -> str:
    """Generate secure random token.
    
    Args:
        length: Token length
    
    Returns:
        Secure random token
    """
    return secrets.token_urlsafe(length)


def hash_string(text: str, algorithm: str = 'sha256') -> str:
    """Hash string using specified algorithm.
    
    Args:
        text: Text to hash
        algorithm: Hash algorithm
    
    Returns:
        Hashed string
    """
    hash_obj = hashlib.new(algorithm)
    hash_obj.update(text.encode('utf-8'))
    return hash_obj.hexdigest()


def mask_sensitive_data(data: str, mask_char: str = '*', 
                       visible_start: int = 2, visible_end: int = 2) -> str:
    """Mask sensitive data showing only start and end characters.
    
    Args:
        data: Data to mask
        mask_char: Character to use for masking
        visible_start: Number of visible characters at start
        visible_end: Number of visible characters at end
    
    Returns:
        Masked data
    """
    if len(data) <= visible_start + visible_end:
        return mask_char * len(data)
    
    start = data[:visible_start]
    end = data[-visible_end:] if visible_end > 0 else ''
    middle = mask_char * (len(data) - visible_start - visible_end)
    
    return f"{start}{middle}{end}"


# ============================================================================
# Cache Utilities
# ============================================================================

def generate_cache_key(*args, prefix: str = 'adtlas') -> str:
    """Generate cache key from arguments.
    
    Args:
        *args: Arguments to include in key
        prefix: Key prefix
    
    Returns:
        Cache key
    """
    key_parts = [str(arg) for arg in args]
    key = ':'.join([prefix] + key_parts)
    return key


# ============================================================================
# Response Utilities
# ============================================================================

def json_response(data: Any = None, success: bool = True, 
                 message: str = '', status: int = 200, 
                 errors: Dict = None) -> JsonResponse:
    """Create standardized JSON response.
    
    Args:
        data: Response data
        success: Success status
        message: Response message
        status: HTTP status code
        errors: Error details
    
    Returns:
        JsonResponse object
    """
    response_data = {
        'success': success,
        'message': message,
        'data': data,
    }
    
    if errors:
        response_data['errors'] = errors
    
    return JsonResponse(response_data, status=status)


def success_response(data: Any = None, message: str = 'Success') -> JsonResponse:
    """Create success JSON response.
    
    Args:
        data: Response data
        message: Success message
    
    Returns:
        JsonResponse object
    """
    return json_response(data=data, success=True, message=message, status=200)


def error_response(message: str = 'Error', errors: Dict = None, status: int = 400) -> JsonResponse:
    """Create error JSON response.
    
    Args:
        message: Error message
        errors: Error details
        status: HTTP status code
    
    Returns:
        JsonResponse object
    """
    return json_response(
        success=False, 
        message=message, 
        errors=errors, 
        status=status
    )
