"""Geolocation services for IP-based location detection and caching."""

from dataclasses import dataclass
from datetime import datetime
from typing import Optional
import logging
import time
import re
from django.core.cache import cache
from django.conf import settings
from cms.geo.errors import (
    GeoTargetingError,
    GeolocationProviderError,
    CacheError,
    InvalidIPError,
    NetworkTimeoutError,
    get_error_logger,
    get_recovery_strategy
)

logger = logging.getLogger(__name__)
error_logger = get_error_logger()
recovery_strategy = get_recovery_strategy()


@dataclass
class GeoData:
    """Geolocation data for a visitor.
    
    Attributes:
        country_code: ISO 3166-1 alpha-2 country code (e.g., 'CA', 'GB', 'US')
        country_name: Full country name (e.g., 'Canada', 'United Kingdom')
        is_cached: Whether the result came from cache
        accuracy: Accuracy percentage (0-100)
        timestamp: When the lookup was performed
        provider: Which provider returned this data
    """
    country_code: str
    country_name: str
    is_cached: bool
    accuracy: float
    timestamp: datetime
    provider: str

    def is_canada(self) -> bool:
        """Check if the detected country is Canada."""
        return self.country_code == 'CA'

    def is_uk(self) -> bool:
        """Check if the detected country is United Kingdom."""
        return self.country_code == 'GB'

    def is_ireland(self) -> bool:
        """Check if the detected country is Ireland."""
        return self.country_code == 'IE'

    def is_uae(self) -> bool:
        """Check if the detected country is United Arab Emirates."""
        return self.country_code in ('AE', 'UAE')


class GeoLocator:
    """Service for IP geolocation with caching and provider abstraction.
    
    This service handles IP geolocation lookups with support for multiple providers,
    caching, and graceful fallback to a default region on failures.
    """
    
    # Country code to name mapping for common countries
    COUNTRY_NAMES = {
        'CA': 'Canada',
        'GB': 'United Kingdom',
        'US': 'United States',
        'AU': 'Australia',
        'IE': 'Ireland',
        'AE': 'United Arab Emirates',
        'DE': 'Germany',
        'FR': 'France',
        'JP': 'Japan',
        'IN': 'India',
        'BR': 'Brazil',
        'MX': 'Mexico',
    }
    
    # Default fallback region
    DEFAULT_FALLBACK_COUNTRY = 'GB'
    DEFAULT_FALLBACK_NAME = 'United Kingdom'
    
    # Cache timeout in seconds (5 minutes)
    CACHE_TIMEOUT = 300
    
    def __init__(self, provider: str = 'maxmind', cache_timeout: Optional[int] = None):
        """Initialize the GeoLocator service.
        
        Args:
            provider: The geolocation provider to use ('maxmind' or 'mock' for testing)
            cache_timeout: Cache timeout in seconds (defaults to 300)
        """
        self.provider_name = provider
        self.cache_timeout = cache_timeout or self.CACHE_TIMEOUT
        self._provider = self._initialize_provider(provider)
    
    def _initialize_provider(self, provider: str):
        """Initialize the geolocation provider.
        
        Args:
            provider: Provider name ('maxmind' or 'mock')
            
        Returns:
            The initialized provider instance
        """
        if provider == 'maxmind':
            return MaxMindProvider()
        elif provider == 'mock':
            return MockProvider()
        else:
            raise ValueError(f"Unknown provider: {provider}")
    
    def lookup(self, ip_address: str) -> GeoData:
        """Lookup geolocation for an IP address.
        
        This method checks the cache first, then queries the provider if needed.
        On any failure or for unknown countries, it returns the default fallback region.
        
        Implements comprehensive error handling:
        - Validates IP address format
        - Detects private IP addresses
        - Handles cache failures gracefully
        - Implements retry logic for provider failures
        - Tracks performance metrics
        
        Args:
            ip_address: The IP address to lookup
            
        Returns:
            GeoData object with country code, name, and metadata
        """
        start_time = time.time()
        
        try:
            # Validate IP address format
            if not self._is_valid_ip(ip_address):
                error = InvalidIPError(ip_address, reason='invalid_format')
                error_logger.log_error(error)
                self._log_fallback_event(ip_address, 'invalid_ip_format')
                return self._get_fallback_geo_data(is_cached=False)
            
            # Check if IP is private
            if self._is_private_ip(ip_address):
                logger.debug(f"Private IP address detected: {ip_address}")
                self._log_fallback_event(ip_address, 'private_ip')
                return self._get_fallback_geo_data(is_cached=False)
            
            # Try to get from cache
            geo_data = self._lookup_from_cache(ip_address)
            if geo_data is not None:
                geo_data.is_cached = True
                self._record_lookup_metrics(start_time, is_cached=True)
                return geo_data
            
            # Query provider with retry logic
            geo_data = self._lookup_from_provider(ip_address)
            
            if geo_data:
                # Cache the result
                self._cache_result(ip_address, geo_data)
                self._record_lookup_metrics(start_time, is_cached=False)
                return geo_data
            else:
                # Provider failed, return fallback
                self._log_fallback_event(ip_address, 'provider_failed')
                return self._get_fallback_geo_data(is_cached=False)
                
        except Exception as e:
            # Unexpected error, log and return fallback
            error = GeoTargetingError(
                f"Unexpected error during geolocation lookup: {str(e)}",
                error_code='LOOKUP_ERROR',
                context={'ip_address': ip_address}
            )
            error_logger.log_error(error)
            logger.error(f"Geolocation lookup error for IP {ip_address}: {str(e)}", exc_info=True)
            return self._get_fallback_geo_data(is_cached=False)
    
    def _get_fallback_geo_data(self, is_cached: bool = False) -> GeoData:
        """Get the default fallback geolocation data.
        
        Args:
            is_cached: Whether this result came from cache
            
        Returns:
            GeoData with default fallback region
        """
        return GeoData(
            country_code=self.DEFAULT_FALLBACK_COUNTRY,
            country_name=self.DEFAULT_FALLBACK_NAME,
            is_cached=is_cached,
            accuracy=0.0,
            timestamp=datetime.now(),
            provider='fallback'
        )
    
    def _lookup_from_cache(self, ip_address: str) -> Optional[GeoData]:
        """Lookup geolocation result from cache.
        
        Handles cache failures gracefully by returning None instead of raising.
        
        Args:
            ip_address: The IP address to lookup
            
        Returns:
            GeoData if found in cache, None otherwise
        """
        try:
            cache_key = f'geo:{ip_address}'
            cached_result = cache.get(cache_key)
            return cached_result
        except Exception as e:
            # Cache failure - log but don't fail the lookup
            error = CacheError(
                f"Failed to read from cache: {str(e)}",
                operation='get',
                context={'ip_address': ip_address}
            )
            error_logger.log_error(error)
            logger.warning(f"Cache read error for IP {ip_address}: {str(e)}")
            return None
    
    def _cache_result(self, ip_address: str, geo_data: GeoData):
        """Cache a geolocation result.
        
        Handles cache failures gracefully by logging but not raising.
        
        Args:
            ip_address: The IP address
            geo_data: The geolocation data to cache
        """
        try:
            cache_key = f'geo:{ip_address}'
            cache.set(cache_key, geo_data, self.cache_timeout)
        except Exception as e:
            # Cache failure - log but don't fail the lookup
            error = CacheError(
                f"Failed to write to cache: {str(e)}",
                operation='set',
                context={'ip_address': ip_address}
            )
            error_logger.log_error(error)
            logger.warning(f"Cache write error for IP {ip_address}: {str(e)}")
    
    def _lookup_from_provider(self, ip_address: str) -> Optional[GeoData]:
        """Lookup geolocation from provider with error handling.
        
        Implements retry logic and circuit breaker pattern for provider failures.
        
        Args:
            ip_address: The IP address to lookup
            
        Returns:
            GeoData if successful, None on failure
        """
        # Check if circuit breaker is open for this provider
        if recovery_strategy.is_circuit_open(self.provider_name):
            logger.warning(
                f"Circuit breaker open for provider {self.provider_name}, "
                f"skipping lookup for IP {ip_address}"
            )
            return None
        
        attempt = 0
        last_error = None
        
        while attempt < recovery_strategy.MAX_RETRIES:
            attempt += 1
            
            try:
                geo_data = self._provider.lookup(ip_address)
                
                # Validate provider result
                if geo_data and geo_data.country_code:
                    # Check if country code is known/supported
                    if not self._is_known_country(geo_data.country_code):
                        logger.warning(
                            f"Unknown country code returned for IP {ip_address}: "
                            f"{geo_data.country_code}"
                        )
                        self._log_fallback_event(ip_address, 'unknown_country', geo_data.country_code)
                        return None
                    
                    # Get full country name if not provided
                    if not geo_data.country_name:
                        geo_data.country_name = self.COUNTRY_NAMES.get(
                            geo_data.country_code,
                            geo_data.country_code
                        )
                    
                    # Record success
                    recovery_strategy.record_success(self.provider_name)
                    return geo_data
                else:
                    logger.warning(f"Provider returned invalid result for IP: {ip_address}")
                    return None
                    
            except Exception as e:
                last_error = e
                
                # Create error object
                error = GeolocationProviderError(
                    f"Provider lookup failed: {str(e)}",
                    provider=self.provider_name,
                    context={'ip_address': ip_address, 'attempt': attempt}
                )
                error_logger.log_error(error)
                
                # Record error for circuit breaker
                recovery_strategy.record_error(self.provider_name)
                
                # Check if should retry
                if attempt < recovery_strategy.MAX_RETRIES:
                    logger.debug(
                        f"Retrying geolocation lookup for IP {ip_address} "
                        f"(attempt {attempt}/{recovery_strategy.MAX_RETRIES})"
                    )
                    time.sleep(recovery_strategy.RETRY_DELAY_SECONDS)
                else:
                    logger.error(
                        f"Geolocation provider error for IP {ip_address} "
                        f"after {attempt} attempts: {str(e)}"
                    )
        
        return None
    
    def _is_known_country(self, country_code: str) -> bool:
        """Check if a country code is known/supported.
        
        This method validates that the country code is in our known countries list.
        Unknown countries trigger a fallback to the default region.
        
        Args:
            country_code: The ISO 3166-1 alpha-2 country code to check
            
        Returns:
            True if the country is known, False otherwise
        """
        if not country_code:
            return False
        
        # Check if country code is in our known countries mapping
        return country_code in self.COUNTRY_NAMES
    
    def _log_fallback_event(self, ip_address: str, reason: str, details: str = None):
        """Log a fallback event for monitoring and debugging.
        
        This method logs when the system falls back to the default region,
        which helps identify issues with geolocation or provider failures.
        
        Args:
            ip_address: The IP address that triggered the fallback
            reason: The reason for fallback (e.g., 'invalid_ip_format', 'unknown_country', 'provider_exception')
            details: Optional additional details about the fallback event
        """
        log_message = f"Fallback to {self.DEFAULT_FALLBACK_COUNTRY} for IP {ip_address}: reason={reason}"
        
        if details:
            log_message += f", details={details}"
        
        logger.info(log_message)
    
    def _record_lookup_metrics(self, start_time: float, is_cached: bool = False):
        """Record performance metrics for a geolocation lookup.
        
        Args:
            start_time: The time when the lookup started (from time.time())
            is_cached: Whether the result came from cache
        """
        try:
            from cms.geo.metrics import get_metrics
            
            # Calculate duration in milliseconds
            duration_ms = (time.time() - start_time) * 1000
            
            # Record the lookup time
            metrics = get_metrics()
            metrics.record_geolocation_lookup_time(duration_ms, is_cached=is_cached)
            
            # Record cache hit/miss
            if is_cached:
                metrics.record_cache_hit()
            else:
                metrics.record_cache_miss()
                
        except Exception as e:
            logger.error(f"Error recording lookup metrics: {str(e)}", exc_info=True)
    
    def _is_valid_ip(self, ip_address: str) -> bool:
        """Check if the IP address format is valid.
        
        Args:
            ip_address: The IP address to validate
            
        Returns:
            True if valid, False otherwise
        """
        if not ip_address or not isinstance(ip_address, str):
            return False
        
        # Basic validation - check for IPv4 or IPv6 format
        parts = ip_address.split('.')
        if len(parts) == 4:
            # IPv4
            try:
                return all(0 <= int(part) <= 255 for part in parts)
            except ValueError:
                return False
        elif ':' in ip_address:
            # IPv6 - basic check
            return len(ip_address) > 0
        
        return False
    
    def _is_private_ip(self, ip_address: str) -> bool:
        """Check if the IP address is in a private range.
        
        Args:
            ip_address: The IP address to check
            
        Returns:
            True if private, False otherwise
        """
        # Private IPv4 ranges
        private_ranges = [
            ('127.0.0.0', '127.255.255.255'),      # Loopback
            ('10.0.0.0', '10.255.255.255'),        # Private
            ('172.16.0.0', '172.31.255.255'),      # Private
            ('192.168.0.0', '192.168.255.255'),    # Private
            ('169.254.0.0', '169.254.255.255'),    # Link-local
        ]
        
        try:
            ip_parts = [int(x) for x in ip_address.split('.')]
            if len(ip_parts) != 4:
                return False
            
            ip_int = (ip_parts[0] << 24) + (ip_parts[1] << 16) + (ip_parts[2] << 8) + ip_parts[3]
            
            for start, end in private_ranges:
                start_parts = [int(x) for x in start.split('.')]
                end_parts = [int(x) for x in end.split('.')]
                
                start_int = (start_parts[0] << 24) + (start_parts[1] << 16) + (start_parts[2] << 8) + start_parts[3]
                end_int = (end_parts[0] << 24) + (end_parts[1] << 16) + (end_parts[2] << 8) + end_parts[3]
                
                if start_int <= ip_int <= end_int:
                    return True
            
            return False
        except (ValueError, IndexError):
            return False


class MaxMindProvider:
    """MaxMind GeoIP2 provider for IP geolocation."""
    
    def __init__(self):
        """Initialize the MaxMind provider."""
        self.db_path = getattr(settings, 'GEOIP2_DB_PATH', None)
        self._reader = None
        
        if self.db_path:
            try:
                import geoip2.database
                self._reader = geoip2.database.Reader(self.db_path)
            except Exception as e:
                logger.warning(f"Failed to initialize MaxMind reader: {str(e)}")
    
    def lookup(self, ip_address: str) -> Optional[GeoData]:
        """Lookup geolocation using MaxMind GeoIP2.
        
        Args:
            ip_address: The IP address to lookup
            
        Returns:
            GeoData object or None on failure
        """
        if not self._reader:
            logger.warning("MaxMind reader not initialized")
            return None
        
        try:
            # Use country() method for GeoLite2-Country database
            response = self._reader.country(ip_address)
            
            return GeoData(
                country_code=response.country.iso_code,
                country_name=response.country.name,
                is_cached=False,
                accuracy=0,  # Country database doesn't provide accuracy_radius
                timestamp=datetime.now(),
                provider='maxmind'
            )
        except Exception as e:
            logger.error(f"MaxMind lookup failed for {ip_address}: {str(e)}")
            return None


class MockProvider:
    """Mock provider for testing purposes."""
    
    def __init__(self):
        """Initialize the mock provider."""
        self.lookup_count = 0
    
    def lookup(self, ip_address: str) -> Optional[GeoData]:
        """Mock geolocation lookup.
        
        Args:
            ip_address: The IP address to lookup
            
        Returns:
            GeoData object with mock data
        """
        self.lookup_count += 1
        
        # Simple mock: return Canada for IPs starting with 1, UK otherwise
        if ip_address.startswith('1'):
            country_code = 'CA'
            country_name = 'Canada'
        else:
            country_code = 'GB'
            country_name = 'United Kingdom'
        
        return GeoData(
            country_code=country_code,
            country_name=country_name,
            is_cached=False,
            accuracy=95.0,
            timestamp=datetime.now(),
            provider='mock'
        )


class HrefLangValidator:
    """Service for validating hreflang tags.
    
    This service validates hreflang tags to ensure they conform to:
    - Valid HTML syntax
    - Valid language codes (RFC 5646 format)
    - Valid absolute URLs
    - Proper tag structure
    
    Validation is essential to ensure search engines can correctly parse
    and understand hreflang tags.
    """
    
    # Valid hreflang language codes
    VALID_LANG_CODES = {
        'en-CA', 'en-GB', 'en-US', 'en-AU', 'en-IE', 'en-AE',
        'fr-CA', 'fr-FR', 'de-DE', 'es-ES', 'it-IT', 'ja-JP',
        'zh-CN', 'zh-TW', 'pt-BR', 'pt-PT', 'ru-RU', 'ko-KR',
        'x-default'  # Special fallback code
    }
    
    def __init__(self):
        """Initialize the HrefLangValidator service."""
        pass
    
    def validate_tag(self, tag: str) -> tuple:
        """Validate a single hreflang tag.
        
        This method validates that a tag conforms to proper hreflang syntax
        and contains valid values.
        
        Args:
            tag: The HTML tag string to validate
            
        Returns:
            Tuple of (is_valid: bool, errors: list)
            - is_valid: True if tag is valid, False otherwise
            - errors: List of validation error messages
        """
        errors = []
        
        # Check if tag is a string
        if not isinstance(tag, str):
            errors.append("Tag must be a string")
            return False, errors
        
        # Check if tag is empty
        if not tag or not tag.strip():
            errors.append("Tag cannot be empty")
            return False, errors
        
        # Check basic HTML structure
        if not tag.startswith('<link'):
            errors.append("Tag must start with '<link'")
        
        if not tag.endswith('/>'):
            errors.append("Tag must end with '/>'")
        
        # Check for required attributes
        if 'rel="alternate"' not in tag:
            errors.append("Tag must include rel=\"alternate\" attribute")
        
        if 'hreflang=' not in tag:
            errors.append("Tag must include hreflang attribute")
        
        if 'href=' not in tag:
            errors.append("Tag must include href attribute")
        
        # If there are structural errors, return early
        if errors:
            return False, errors
        
        # Validate hreflang value
        hreflang_errors = self._validate_hreflang_value(tag)
        errors.extend(hreflang_errors)
        
        # Validate href value
        href_errors = self._validate_href_value(tag)
        errors.extend(href_errors)
        
        # Return validation result
        is_valid = len(errors) == 0
        return is_valid, errors
    
    def validate_tags(self, tags: list) -> tuple:
        """Validate a list of hreflang tags.
        
        This method validates all tags in a list and returns comprehensive
        validation results.
        
        Args:
            tags: List of HTML tag strings to validate
            
        Returns:
            Tuple of (is_valid: bool, results: dict)
            - is_valid: True if all tags are valid, False otherwise
            - results: Dictionary with validation details for each tag
        """
        if not isinstance(tags, list):
            return False, {"error": "Tags must be a list"}
        
        if not tags:
            return False, {"error": "Tags list cannot be empty"}
        
        results = {}
        all_valid = True
        
        for i, tag in enumerate(tags):
            is_valid, errors = self.validate_tag(tag)
            results[f"tag_{i}"] = {
                "valid": is_valid,
                "errors": errors,
                "tag": tag
            }
            
            if not is_valid:
                all_valid = False
        
        return all_valid, results
    
    def _validate_hreflang_value(self, tag: str) -> list:
        """Validate the hreflang attribute value.
        
        Args:
            tag: The HTML tag string
            
        Returns:
            List of validation error messages
        """
        import re
        
        errors = []
        
        # Extract hreflang value
        hreflang_match = re.search(r'hreflang="([^"]*)"', tag)
        
        if not hreflang_match:
            errors.append("Invalid hreflang attribute format")
            return errors
        
        lang_code = hreflang_match.group(1)
        
        # Check if hreflang value is empty
        if not lang_code:
            errors.append("hreflang value cannot be empty")
            return errors
        
        # Check if language code is valid
        if lang_code not in self.VALID_LANG_CODES:
            # Allow custom language codes in RFC 5646 format (e.g., en-US, fr-CA)
            if not self._is_valid_language_code(lang_code):
                errors.append(
                    f"Invalid language code: {lang_code}. "
                    f"Must be in RFC 5646 format (e.g., en-US, fr-CA) or 'x-default'"
                )
        
        return errors
    
    def _validate_href_value(self, tag: str) -> list:
        """Validate the href attribute value.
        
        Args:
            tag: The HTML tag string
            
        Returns:
            List of validation error messages
        """
        import re
        
        errors = []
        
        # Extract href value
        href_match = re.search(r'href="([^"]*)"', tag)
        
        if not href_match:
            errors.append("Invalid href attribute format")
            return errors
        
        url = href_match.group(1)
        
        # Check if href value is empty
        if not url:
            errors.append("href value cannot be empty")
            return errors
        
        # Check if URL is absolute
        if not (url.startswith('http://') or url.startswith('https://')):
            errors.append(
                f"href must be an absolute URL (starting with http:// or https://). "
                f"Got: {url}"
            )
        
        # Check for valid URL format (basic validation)
        if not self._is_valid_url(url):
            errors.append(f"Invalid URL format: {url}")
        
        return errors
    
    def _is_valid_language_code(self, lang_code: str) -> bool:
        """Check if a language code is valid RFC 5646 format.
        
        Valid formats:
        - en (language only)
        - en-US (language-region)
        - en-US-x-twain (language-region-private)
        - x-default (special fallback)
        
        Args:
            lang_code: The language code to validate
            
        Returns:
            True if valid, False otherwise
        """
        import re
        
        # RFC 5646 language tag pattern
        # Simplified pattern: language[-region][-script][-variant]
        pattern = r'^[a-z]{2,3}(-[A-Z]{2})?(-[A-Z]{4})?(-[a-z]{2,8})?$|^x-default$'
        
        return bool(re.match(pattern, lang_code, re.IGNORECASE))
    
    def _is_valid_url(self, url: str) -> bool:
        """Check if a URL is valid.
        
        Args:
            url: The URL to validate
            
        Returns:
            True if valid, False otherwise
        """
        import re
        
        # Basic URL validation pattern
        url_pattern = r'^https?://[^\s/$.?#].[^\s]*$'
        
        return bool(re.match(url_pattern, url, re.IGNORECASE))


class HrefLangGenerator:
    """Service for generating hreflang tags for international SEO.
    
    This service generates hreflang tags that signal to search engines which
    language/region version of a page is appropriate. It handles:
    - Mapping regions to hreflang language codes (en-GB, en-CA, x-default)
    - Generating absolute URLs with correct protocol and domain
    - Including x-default fallback tag
    - Validating tag syntax
    
    hreflang tags are essential for international SEO to:
    - Prevent duplicate content penalties
    - Help search engines understand regional versions
    - Improve user experience by showing correct regional content
    """
    
    # Mapping of region codes to hreflang language codes
    REGION_TO_HREFLANG = {
        'CA': 'en-CA',      # Canada
        'GB': 'en-GB',      # United Kingdom
        'US': 'en-US',      # United States
        'AU': 'en-AU',      # Australia
        'IE': 'en-IE',      # Ireland
        'UAE': 'en-AE',     # United Arab Emirates
    }
    
    # Available regions for hreflang tags
    AVAILABLE_REGIONS = ['CA', 'GB', 'IE', 'UAE']
    
    # City landing pages are regional and each city slug exists in ONLY one region.
    # hreflang alternates for a city must therefore be restricted to the owning
    # region, otherwise we fabricate URLs like /ca/city/london/ that 404.
    CITY_SLUGS_BY_REGION = {
        'CA': {'toronto', 'vancouver'},
        'GB': {'london', 'manchester', 'birmingham'},
        'UAE': {'dubai', 'abu-dhabi'},
        'IE': {'dublin', 'cork', 'galway', 'limerick'},
    }
    
    # URL patterns that are UK-only (should only have en-GB hreflang, plus x-default)
    # These patterns match paths that should NOT have regional alternates for CA, UAE, IE
    UK_ONLY_PATTERNS = [
        r'^/risk-management/details/[^/]+/$',  # Risk management detail pages
        r'^/logistics/details/[^/]+/$',        # Logistics detail pages
        # Policy pages are global (available in all regions)
    ]
    
    # Default region for x-default tag
    DEFAULT_REGION = 'GB'
    
    def __init__(self):
        """Initialize the HrefLangGenerator service."""
        self.validator = HrefLangValidator()
    
    def generate_tags(self, request, current_region: Optional[str] = None) -> list:
        """Generate hreflang tags for the current page.
        
        This method generates a complete set of hreflang tags for all available
        regional versions of the current page, plus an x-default tag.
        
        The tags are returned as HTML link elements ready to be inserted into
        the <head> section of the page.
        
        Args:
            request: The HTTP request object (used to build absolute URLs)
            current_region: The current region code (optional, for context)
            
        Returns:
            List of HTML link tag strings for hreflang tags
            
        Example:
            >>> tags = generator.generate_tags(request, 'CA')
            >>> for tag in tags:
            ...     print(tag)
            <link rel="alternate" hreflang="en-CA" href="https://example.com/ca/" />
            <link rel="alternate" hreflang="en-GB" href="https://example.com/" />
            <link rel="alternate" hreflang="x-default" href="https://example.com/" />
        """
        tags = []
        
        # City pages are region-specific: a given city slug exists in exactly one
        # region (e.g. '/city/london/' only on GB; '/ca/city/toronto/' only on CA).
        # For those pages we must NOT emit alternates for other regions, because
        # the fabricated URLs (e.g. '/ca/city/london/') return 404. We restrict
        # alternates to the region that owns the city (plus x-default).
        city_slug = self._get_city_slug(request.path)
        owning_region = None
        if city_slug:
            for region, slugs in self.CITY_SLUGS_BY_REGION.items():
                if city_slug in slugs:
                    owning_region = region
                    break
        
        # Check if this is a UK-only page (should only have en-GB hreflang)
        is_uk_only = False
        for pattern in self.UK_ONLY_PATTERNS:
            if re.match(pattern, request.path):
                is_uk_only = True
                break
        
        # Determine which regions have a genuine version of the current page.
        if city_slug and owning_region:
            regions_to_emit = [owning_region]
        elif is_uk_only:
            # UK-only pages should only have en-GB hreflang
            regions_to_emit = ['GB']
        else:
            # Homepage and all shared pages resolve under every region prefix (the
            # regional url configs include cms.urls), so all regions are valid.
            regions_to_emit = list(self.AVAILABLE_REGIONS)
        
        # Generate tags for each valid region
        for region in self.AVAILABLE_REGIONS:
            if region not in regions_to_emit:
                continue
            url = self._build_regional_url(request, region)
            lang_code = self.REGION_TO_HREFLANG.get(region, region)
            
            tag = self._create_hreflang_tag(lang_code, url)
            tags.append(tag)
        
        # Add x-default tag pointing to the owning region (city pages) or default
        # region (all other pages). For a city page, self-referential alternates
        # point to the city URL itself, which is the authoritative variant.
        if city_slug and owning_region:
            default_url = self._build_regional_url(request, owning_region)
        else:
            default_url = self._build_regional_url(request, self.DEFAULT_REGION)
        default_tag = self._create_hreflang_tag('x-default', default_url)
        tags.append(default_tag)
        
        return tags
    
    def _get_city_slug(self, path: str) -> Optional[str]:
        """Extract the city slug from a city landing-page path, or None."""
        if '/city/' not in path:
            return None
        parts = [p for p in path.split('/') if p]
        if 'city' in parts:
            idx = parts.index('city')
            if idx + 1 < len(parts):
                return parts[idx + 1]
        return None
    
    def _build_regional_url(self, request, region: str) -> str:
        """Build an absolute URL for a specific region.
        
        This method constructs the full URL for a page in a specific region,
        including the correct protocol and domain.
        
        Args:
            request: The HTTP request object
            region: The region code (e.g., 'CA', 'GB')
            
        Returns:
            Absolute URL for the regional version of the page
        """
        # Get the current path
        current_path = request.path
        
        region_prefixes = {
            'CA': '/ca',
            'GB': '',
            'IE': '/ireland',
            'UAE': '/uae',
        }
        region_prefix = region_prefixes.get(region, '')

        # Normalize current path to a base path without any region prefix
        base_path = current_path
        for prefix in ('/ca/', '/uk/', '/uae/', '/ireland/'):
            if base_path.startswith(prefix):
                base_path = '/' + '/'.join(base_path.split('/')[2:])
                break

        if not base_path:
            base_path = '/'

        # Build target path for requested region
        if region_prefix:
            if base_path == '/':
                target_path = region_prefix + '/'
            else:
                target_path = region_prefix + base_path
        else:
            target_path = base_path
        
        # Ensure path ends with / for consistency
        if target_path and not target_path.endswith('/'):
            target_path += '/'
        
        # Build absolute URL. Use the canonical non-www host in production so
        # hreflang hrefs always agree with the hardcoded <link rel="canonical">,
        # regardless of which host the crawler used to reach the page.
        canonical_host = getattr(settings, "CANONICAL_HOST", "").strip().strip("/").lower()
        if canonical_host and not settings.DEBUG:
            absolute_url = f"https://{canonical_host}{target_path}"
        else:
            absolute_url = request.build_absolute_uri(target_path)
        
        return absolute_url
    
    def _create_hreflang_tag(self, lang_code: str, url: str) -> str:
        """Create an HTML hreflang link tag.
        
        This method generates a properly formatted HTML link tag for hreflang.
        
        Args:
            lang_code: The language/region code (e.g., 'en-CA', 'x-default')
            url: The absolute URL for this language/region
            
        Returns:
            HTML link tag string
        """
        # Escape URL to prevent HTML injection
        safe_url = url.replace('"', '&quot;')
        
        tag = f'<link rel="alternate" hreflang="{lang_code}" href="{safe_url}" />'
        
        return tag
    
    def get_lang_code(self, region: str) -> str:
        """Map a region code to its hreflang language code.
        
        Args:
            region: The region code (e.g., 'CA', 'GB')
            
        Returns:
            The hreflang language code (e.g., 'en-CA', 'en-GB')
        """
        return self.REGION_TO_HREFLANG.get(region, region)
    
    def get_available_regions(self, path: str) -> list:
        """Get the list of available regions for a given path.
        
        This method determines which regions have versions of the current page.
        
        Args:
            path: The URL path
            
        Returns:
            List of available region codes
        """
        # Check if this is a city page
        city_slug = self._get_city_slug(path)
        if city_slug:
            # Find which region owns this city
            for region, slugs in self.CITY_SLUGS_BY_REGION.items():
                if city_slug in slugs:
                    return [region]
        
        # Check if this is a UK-only page
        for pattern in self.UK_ONLY_PATTERNS:
            if re.match(pattern, path):
                return ['GB']
        
        # Default: available in all regions
        return self.AVAILABLE_REGIONS.copy()
    
    def validate_generated_tags(self, tags: list) -> tuple:
        """Validate all generated hreflang tags.
        
        This method validates that all generated tags conform to proper
        hreflang syntax and contain valid values.
        
        Args:
            tags: List of generated hreflang tag strings
            
        Returns:
            Tuple of (is_valid: bool, results: dict)
            - is_valid: True if all tags are valid, False otherwise
            - results: Dictionary with validation details
        """
        return self.validator.validate_tags(tags)
    
    def validate_tag(self, tag: str) -> tuple:
        """Validate a single hreflang tag.
        
        Args:
            tag: The HTML tag string to validate
            
        Returns:
            Tuple of (is_valid: bool, errors: list)
        """
        return self.validator.validate_tag(tag)



class GeoRedirector:
    """Service for determining geo-based redirects and generating redirect responses.
    
    This service handles the logic for deciding when to redirect visitors based on
    their detected location, and generates appropriate HTTP redirect responses with
    proper status codes and URL handling.
    
    HTTP Status Codes:
    - 302 (Found): Used for temporary, geo-based redirects based on visitor location
    - 301 (Moved Permanently): Used for permanent URL mappings and restructuring
    """
    
    # URL patterns that should not trigger geo redirects
    NO_REDIRECT_PREFIXES = (
        '/admin/',
        '/robots.txt',
        '/sitemap.xml',
        '/llms.txt',
        '/static/',
        '/media/',
    )
    
    # Region-specific URL patterns that should not trigger redirects
    REGION_SPECIFIC_PATTERNS = [
        '/ca/',      # Canada-specific URLs
        '/uk/',      # UK-specific URLs
        '/uae/',     # UAE-specific URLs
        '/ireland/', # Ireland-specific URLs
    ]
    
    # HTTP Status Codes
    HTTP_STATUS_TEMPORARY_REDIRECT = 302  # For geo-based redirects
    HTTP_STATUS_PERMANENT_REDIRECT = 301  # For permanent URL mappings
    
    def __init__(self):
        """Initialize the GeoRedirector service."""
        pass
    
    def should_redirect(self, request, geo_data: GeoData) -> bool:
        """Determine if a redirect is needed for this request.
        
        Redirect logic:
        - Do NOT redirect if the URL is already region-specific (e.g., /ca/, /uk/)
        - DO redirect Canadian visitors to /ca/ when accessing non-region-specific URLs
        - DO NOT redirect other visitors (they get the default UK content)
        
        Tracks performance metrics for redirect decision time.
        
        Args:
            request: The HTTP request object
            geo_data: The geolocation data for the visitor
            
        Returns:
            True if redirect should occur, False otherwise
        """
        start_time = time.time()
        
        # Skip redirect for excluded paths
        if self._is_no_redirect_path(request.path):
            self._record_redirect_decision_metrics(start_time)
            return False
        
        # Check if URL is already region-specific
        if self._is_region_specific_url(request.path):
            self._record_redirect_decision_metrics(start_time)
            return False
        
        # Check if visitor is from a region with a dedicated version
        if geo_data.is_canada() or geo_data.is_uae() or geo_data.is_ireland():
            self._record_redirect_decision_metrics(start_time)
            return True
        
        self._record_redirect_decision_metrics(start_time)
        return False
    
    def get_redirect_response(self, request, geo_data: GeoData, is_permanent: bool = False):
        """Generate a redirect response for the visitor.
        
        This method generates an HTTP redirect response with the appropriate status code:
        - 302 (temporary) for geo-based redirects
        - 301 (permanent) for permanent URL mappings
        
        The Location header is automatically included by Django's HttpResponseRedirect.
        
        Args:
            request: The HTTP request object
            geo_data: The geolocation data for the visitor
            is_permanent: If True, use 301 (permanent); if False, use 302 (temporary)
            
        Returns:
            HttpResponseRedirect with appropriate status code and Location header
        """
        from django.http import HttpResponseRedirect
        
        target_url = self.build_target_url(request, geo_data)
        
        # Select HTTP status code based on redirect type
        status_code = self._get_http_status_code(is_permanent)
        
        # Create redirect response with Location header
        response = HttpResponseRedirect(target_url, status=status_code)
        
        # Verify Location header is set (Django does this automatically)
        logger.debug(
            f"Generated redirect response: status={status_code}, "
            f"location={response.get('Location', 'NOT SET')}"
        )
        
        return response
    
    def _get_http_status_code(self, is_permanent: bool = False) -> int:
        """Determine the appropriate HTTP status code for a redirect.
        
        Args:
            is_permanent: If True, return 301 (permanent); if False, return 302 (temporary)
            
        Returns:
            HTTP status code (301 or 302)
        """
        if is_permanent:
            return self.HTTP_STATUS_PERMANENT_REDIRECT  # 301
        else:
            return self.HTTP_STATUS_TEMPORARY_REDIRECT  # 302
    
    def build_target_url(self, request, geo_data: GeoData) -> str:
        """Build the target URL for a redirect, preserving query parameters and path.
        
        This method:
        1. Determines the target region prefix based on detected country
        2. Preserves the original path (e.g., /services/ becomes /ca/services/)
        3. Preserves all query parameters from the original request
        4. Handles URL fragments
        
        Args:
            request: The HTTP request object
            geo_data: The geolocation data for the visitor
            
        Returns:
            The complete target URL with region prefix, path, and query parameters preserved
        """
        # Get the original path
        original_path = request.path
        
        # Determine region prefix based on country
        if geo_data.is_canada():
            region_prefix = '/ca'
        elif geo_data.is_uae():
            region_prefix = '/uae'
        elif geo_data.is_ireland():
            region_prefix = '/ireland'
        else:
            region_prefix = ''
        
        # Build the target path by prepending region prefix to original path
        # Remove leading slash from original path if it exists to avoid double slashes
        if original_path.startswith('/'):
            target_path = region_prefix + original_path
        else:
            target_path = region_prefix + '/' + original_path
        
        # Build the target URL
        target_url = request.build_absolute_uri(target_path)
        
        # Preserve query parameters
        if request.GET:
            query_string = request.GET.urlencode()
            target_url = f"{target_url}?{query_string}"
        
        return target_url
    
    def _is_region_specific_url(self, path: str) -> bool:
        """Check if a URL path is region-specific.
        
        Region-specific URLs should not trigger redirects, as the user
        has explicitly chosen a region.
        
        Args:
            path: The URL path to check
            
        Returns:
            True if the path is region-specific, False otherwise
        """
        # Normalize path to lowercase for comparison
        path_lower = path.lower()
        
        # Check against known region-specific patterns
        for pattern in self.REGION_SPECIFIC_PATTERNS:
            if path_lower.startswith(pattern):
                return True
        
        return False

    def _is_no_redirect_path(self, path: str) -> bool:
        """Check if a URL path should bypass geo redirects."""
        if not path:
            return False
        
        path_lower = path.lower()
        for prefix in self.NO_REDIRECT_PREFIXES:
            if path_lower.startswith(prefix):
                return True
        
        return False
    
    def _record_redirect_decision_metrics(self, start_time: float):
        """Record performance metrics for a redirect decision.
        
        Args:
            start_time: The time when the decision started (from time.time())
        """
        try:
            from cms.geo.metrics import get_metrics
            
            # Calculate duration in milliseconds
            duration_ms = (time.time() - start_time) * 1000
            
            # Record the decision time
            metrics = get_metrics()
            metrics.record_redirect_decision_time(duration_ms)
                
        except Exception as e:
            logger.error(f"Error recording redirect decision metrics: {str(e)}", exc_info=True)
