"""Filesystem cache for storing AI-resolved selectors."""

import os
import json
import logging
from typing import Optional, Dict, Tuple
from urllib.parse import urlparse

from lamia.engine.config_provider import ConfigProvider

logger = logging.getLogger(__name__)


class AISelectorCache:
    """Filesystem-based cache for AI-resolved selectors to avoid repeated LLM calls."""
    
    def __init__(
        self,
        config_provider: ConfigProvider,
    ):
        """Initialize the cache.
        
        Args:
            cache_enabled: Whether caching is enabled
            cache_dir_name: Name of cache directory
            config_provider: Configuration provider for cache settings
        """
        self.cache_enabled = config_provider.is_cache_enabled()
        self.cache_dir_name = config_provider.get_cache_dir()
        self.cache_file_name = 'selector_resolutions.json'
        self._cache_data = None  # Lazy loaded
    
    async def get(self, original_selector: str, page_url: str, parent_context: Optional[str] = None) -> Optional[str]:
        """Get cached resolved selector.
        
        Args:
            original_selector: The original selector that was resolved
            page_url: URL of the page where selector was used
            parent_context: Optional parent element context (e.g., "fieldset", "div.modal")
            
        Returns:
            Cached resolved selector or None if not found
        """
        if not self.cache_enabled:
            return None
            
        cache_data = self._load_cache()
        cache_key = self._create_cache_key(original_selector, page_url, parent_context)
        
        resolved = cache_data.get(cache_key)
        if resolved:
            logger.debug(f"Cache hit for selector '{original_selector}' on {page_url} (context: {parent_context})")
        else:
            logger.debug(f"Cache miss for selector '{original_selector}' on {page_url} (context: {parent_context})")
            
        return resolved
    
    async def set(self, original_selector: str, page_url: str, resolved_selector: str, parent_context: Optional[str] = None) -> None:
        """Store resolved selector in cache.
        
        Args:
            original_selector: The original selector that was resolved
            page_url: URL of the page where selector was used
            resolved_selector: The AI-resolved selector
            parent_context: Optional parent element context (e.g., "fieldset", "div.modal")
        """
        if not self.cache_enabled:
            return
            
        if not resolved_selector or not resolved_selector.strip():
            logger.warning(f"Not caching empty resolved selector for '{original_selector}'")
            return
            
        cache_data = self._load_cache()
        cache_key = self._create_cache_key(original_selector, page_url, parent_context)
        cache_data[cache_key] = resolved_selector.strip()
        
        self._save_cache(cache_data)
        logger.info(f"Saved selector to cache: '{original_selector}' -> '{resolved_selector}' for {page_url} (context: {parent_context})")
    
    @staticmethod
    def _extract_hostname(url: str) -> str:
        """Extract hostname from URL for stable cache keys across page navigations."""
        try:
            parsed = urlparse(url)
            if parsed.hostname:
                return parsed.hostname
        except Exception:
            pass
        return url

    def _create_cache_key(self, original_selector: str, page_url: str, parent_context: Optional[str] = None) -> str:
        """Create cache key from selector and hostname.

        Uses only the hostname (not full URL) so the same selector resolution
        is reused across different pages on the same site.

        parent_context is accepted for API compatibility but NOT included in the
        key.
        """
        host = self._extract_hostname(page_url)
        return f"{original_selector}|{host}"
    
    def _get_cache_file_path(self) -> str:
        """Get path to the selector cache file."""
        cache_dir = os.path.join(os.getcwd(), self.cache_dir_name, 'selectors')
        return os.path.join(cache_dir, self.cache_file_name)
    
    def _load_cache(self) -> Dict[str, str]:
        """Load cache data from file."""
        if self._cache_data is not None:
            return self._cache_data
            
        cache_file_path = self._get_cache_file_path()
        
        try:
            if os.path.exists(cache_file_path):
                with open(cache_file_path, 'r') as f:
                    self._cache_data = json.load(f)
            else:
                self._cache_data = {}
        except (OSError, IOError, json.JSONDecodeError) as e:
            logger.warning(f"Failed to load cache file {cache_file_path}: {e}")
            self._cache_data = {}
        
        return self._cache_data
    
    def _save_cache(self, cache_data: Dict[str, str]) -> None:
        """Save cache data to file."""
        cache_file_path = self._get_cache_file_path()
        
        try:
            cache_dir = os.path.dirname(cache_file_path)
            os.makedirs(cache_dir, exist_ok=True)
            
            with open(cache_file_path, 'w') as f:
                json.dump(cache_data, f, indent=2)
                
            self._cache_data = cache_data  # Update in-memory copy
            logger.info(f"Cache file saved to: {cache_file_path} with {len(cache_data)} entries")
        except (OSError, IOError) as e:
            logger.warning(f"Failed to save cache file {cache_file_path}: {e}")
    
    def clear(self) -> None:
        """Clear all cached entries."""
        if not self.cache_enabled:
            return
            
        cache_file_path = self._get_cache_file_path()
        cleared_count = 0
        
        if os.path.exists(cache_file_path):
            try:
                with open(cache_file_path, 'r') as f:
                    cache_data = json.load(f)
                    cleared_count = len(cache_data)
                    
                # Write empty cache
                self._save_cache({})
                logger.info(f"Cleared {cleared_count} cached selector resolutions")
            except (OSError, IOError, json.JSONDecodeError) as e:
                logger.warning(f"Failed to clear cache: {e}")
        
        self._cache_data = {}
    
    def size(self) -> int:
        """Get current cache size.
        
        Returns:
            Number of cached entries
        """
        if not self.cache_enabled:
            return 0
            
        cache_data = self._load_cache()
        return len(cache_data)
    
    async def invalidate(self, original_selector: str, page_url: str, parent_context: Optional[str] = None) -> None:
        """Invalidate cached selector resolutions.

        Removes ALL entries that match the selector and URL regardless of
        parent_context so that stale resolutions stored under any context
        variant are cleaned up.
        
        Args:
            original_selector: The original selector to invalidate
            page_url: URL of the page where selector was used
            parent_context: Ignored — all context variants are removed
        """
        if not self.cache_enabled:
            return

        cache_data = self._load_cache()
        prefix = f"{original_selector}|"
        keys_to_remove = [k for k in cache_data if k.startswith(prefix)]

        for key in keys_to_remove:
            del cache_data[key]

        if keys_to_remove:
            self._save_cache(cache_data)
            logger.info(f"Invalidated {len(keys_to_remove)} cached entries for selector '{original_selector}'")
        else:
            logger.debug(f"No cached entry to invalidate for: '{original_selector}' on {page_url}")
    
    async def reset_for_description(self, description: str) -> int:
        """Reset cache entries matching a specific description.
        
        Args:
            description: The selector description to match
            
        Returns:
            Number of entries removed
        """
        if not self.cache_enabled:
            return 0
            
        cache_data = self._load_cache()
        keys_to_remove = [k for k in cache_data if description in k.split('|')[0]]
        
        for key in keys_to_remove:
            del cache_data[key]
        
        if keys_to_remove:
            self._save_cache(cache_data)
            logger.info(f"Reset cache for description '{description}': removed {len(keys_to_remove)} entries")
        
        return len(keys_to_remove)
    
    async def reset_for_url(self, url: str) -> int:
        """Reset cache entries for a specific URL.
        
        Args:
            url: The page URL to match
            
        Returns:
            Number of entries removed
        """
        if not self.cache_enabled:
            return 0
            
        cache_data = self._load_cache()
        keys_to_remove = [k for k in cache_data if url in k.split('|')[1]]
        
        for key in keys_to_remove:
            del cache_data[key]
        
        if keys_to_remove:
            self._save_cache(cache_data)
            logger.info(f"Reset cache for URL '{url}': removed {len(keys_to_remove)} entries")
        
        return len(keys_to_remove)
    
    async def reset_all(self) -> int:
        """Reset entire cache.
        
        Returns:
            Number of entries removed
        """
        if not self.cache_enabled:
            return 0
            
        cache_data = self._load_cache()
        count = len(cache_data)
        
        if count > 0:
            self._save_cache({})
            logger.info(f"Reset entire cache: removed {count} entries")
        
        return count
    
    async def show(self) -> Dict[str, Tuple[str, str, str]]:
        """Get all cached selectors in human-readable format.
        
        Returns:
            Dict mapping cache_key to (description, url, resolved_selector)
        """
        if not self.cache_enabled:
            return {}
            
        cache_data = self._load_cache()
        result = {}
        
        for key, resolved_selector in cache_data.items():
            parts = key.split('|', 1)
            if len(parts) == 2:
                description, url = parts
                result[key] = (description, url, resolved_selector)
        
        return result