#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Shared Browser Utilities Module
Provides unified Chrome driver setup, version detection, and session management
for all automation scripts.
"""

import os
import re
import shutil
import subprocess
import time
import logging
from typing import Optional, Dict, Any

# Windows registry access for Chrome version detection
try:
    import winreg
    HAS_WINREG = True
except ImportError:
    HAS_WINREG = False

# Import undetected_chromedriver
try:
    import undetected_chromedriver as uc
except ImportError:
    uc = None
    raise ImportError("undetected_chromedriver is required. Please install it: pip install undetected-chromedriver")


# ============================================================================
# Chrome Binary Detection
# ============================================================================

def _detect_chrome_binary_paths():
    """Detect possible Chrome binary paths across different platforms.
    
    Returns:
        list: List of candidate Chrome binary paths
    """
    candidates = []
    for path in [
        os.environ.get('GOOGLE_CHROME_BIN'),
        '/usr/bin/google-chrome',
        '/usr/bin/google-chrome-stable',
        '/usr/bin/chromium',
        '/usr/bin/chromium-browser',
        '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
        'C:/Program Files/Google/Chrome/Application/chrome.exe',
        'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'
    ]:
        if path and os.path.exists(path):
            candidates.append(path)
    return candidates


def get_installed_chrome_major_version() -> Optional[int]:
    """Detect the major version of installed Chrome/Chromium browser.
    
    Returns:
        int: Major version number (e.g., 141) or None if not detected
    """
    # Try detected binary paths first
    for binary in _detect_chrome_binary_paths():
        try:
            out = subprocess.check_output([binary, '--version'], stderr=subprocess.STDOUT, timeout=5)
            text = out.decode('utf-8', errors='ignore')
            # Match patterns like "Chrome 141.0.7390.123" or "Chromium 141.0.7390.123"
            m = re.search(r'(?:Chrome|Chromium)\s+([0-9]+)\.', text)
            if m:
                version = int(m.group(1))
                return version
        except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, ValueError):
            continue
        except Exception:
            continue
    
    # Fallback: try plain `google-chrome --version` commands
    for cmd in ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser']:
        try:
            out = subprocess.check_output([cmd, '--version'], stderr=subprocess.STDOUT, timeout=5)
            text = out.decode('utf-8', errors='ignore')
            m = re.search(r'([0-9]+)\.', text)
            if m:
                version = int(m.group(1))
                return version
        except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, ValueError):
            pass
        except Exception:
            pass
    
    # Windows-specific: Try reading from registry
    if HAS_WINREG and os.name == 'nt':
        try:
            registry_paths = [
                (winreg.HKEY_CURRENT_USER, r'Software\Google\Chrome\BLBeacon'),
                (winreg.HKEY_LOCAL_MACHINE, r'Software\Google\Chrome\BLBeacon'),
                (winreg.HKEY_CURRENT_USER, r'Software\Google\Update\ClientState\{8A69D345-D564-463C-AFF1-A69D9E530F96}'),
                (winreg.HKEY_LOCAL_MACHINE, r'Software\Google\Update\ClientState\{8A69D345-D564-463C-AFF1-A69D9E530F96}'),
            ]
            
            for hkey, path in registry_paths:
                try:
                    key = winreg.OpenKey(hkey, path)
                    try:
                        version_str, _ = winreg.QueryValueEx(key, 'version')
                        m = re.search(r'([0-9]+)\.', str(version_str))
                        if m:
                            version = int(m.group(1))
                            winreg.CloseKey(key)
                            return version
                    except FileNotFoundError:
                        pass
                    finally:
                        winreg.CloseKey(key)
                except (FileNotFoundError, OSError):
                    continue
        except Exception:
            pass
    
    # Windows-specific: Try reading from Chrome's version file
    if os.name == 'nt':
        version_file_paths = [
            os.path.expanduser(r'~\AppData\Local\Google\Chrome\Application\version'),
            r'C:\Program Files\Google\Chrome\Application\version',
            r'C:\Program Files (x86)\Google\Chrome\Application\version',
        ]
        for version_file in version_file_paths:
            try:
                if os.path.exists(version_file):
                    with open(version_file, 'r', encoding='utf-8', errors='ignore') as f:
                        content = f.read()
                        m = re.search(r'([0-9]+)\.', content)
                        if m:
                            version = int(m.group(1))
                            return version
            except Exception:
                continue
    
    return None


# ============================================================================
# Cache Management
# ============================================================================

def ensure_uc_cache_directory() -> Optional[str]:
    """Ensure undetected_chromedriver cache directory exists with proper permissions.
    
    Returns:
        str: Path to the cache directory that was ensured, or None if failed
    """
    cache_dirs = [
        os.path.expanduser('~/.local/share/undetected_chromedriver'),  # Linux
        os.path.expanduser('~/AppData/Roaming/undetected_chromedriver'),  # Windows
        os.path.expanduser('~/Library/Application Support/undetected_chromedriver'),  # macOS
    ]
    
    for cache_dir in cache_dirs:
        try:
            os.makedirs(cache_dir, mode=0o755, exist_ok=True)
            if os.path.isdir(cache_dir) and os.access(cache_dir, os.W_OK):
                return cache_dir
        except Exception:
            continue
    
    return None


def clear_uc_cache():
    """Clear undetected_chromedriver cache from common locations"""
    cache_dirs = [
        os.path.expanduser('~/.local/share/undetected_chromedriver'),  # Linux
        os.path.expanduser('~/AppData/Roaming/undetected_chromedriver'),  # Windows
        os.path.expanduser('~/Library/Application Support/undetected_chromedriver'),  # macOS
    ]
    for cache_dir in cache_dirs:
        try:
            if os.path.isdir(cache_dir):
                shutil.rmtree(cache_dir, ignore_errors=True)
        except Exception:
            pass


def validate_driver_file(cache_dir: Optional[str] = None, logger: Optional[logging.Logger] = None) -> bool:
    """Validate that ChromeDriver executable exists and is accessible.
    
    Args:
        cache_dir: Optional cache directory path to check
        logger: Optional logger instance for logging
        
    Returns:
        bool: True if driver file appears to exist, False otherwise
    """
    if not cache_dir:
        cache_dir = ensure_uc_cache_directory()
    
    if not cache_dir:
        return False
    
    driver_paths = [
        os.path.join(cache_dir, 'undetected_chromedriver'),
        os.path.join(cache_dir, 'undetected', 'chromedriver-linux64', 'chromedriver'),
        os.path.join(cache_dir, 'undetected', 'chromedriver-win64', 'chromedriver.exe'),
        os.path.join(cache_dir, 'undetected', 'chromedriver-mac-x64', 'chromedriver'),
    ]
    
    for driver_path in driver_paths:
        if os.path.exists(driver_path):
            if os.name != 'nt' and not os.access(driver_path, os.X_OK):
                try:
                    os.chmod(driver_path, 0o755)
                except Exception:
                    pass
            if logger:
                logger.debug(f"✅ Found driver file: {driver_path}")
            return True
    
    return False


def validate_driver_session(driver, logger: Optional[logging.Logger] = None) -> bool:
    """Validate that the ChromeDriver session is actually working.
    
    Args:
        driver: ChromeDriver instance to validate
        logger: Optional logger instance for logging
        
    Returns:
        bool: True if session is working, False otherwise
    """
    try:
        _ = driver.current_url
        return True
    except Exception as e:
        error_msg = str(e)
        if logger:
            if "Connection refused" in error_msg or "Failed to establish" in error_msg:
                logger.warning(f"⚠️ Driver session connection error: {error_msg}")
            else:
                logger.warning(f"⚠️ Driver session validation failed: {error_msg}")
        return False


# ============================================================================
# Chrome Options Builder
# ============================================================================

def build_default_options(
    headless: bool = True,
    user_data_dir: Optional[str] = None,
    window_size: str = '1366,768',
    incognito: bool = True
) -> uc.ChromeOptions:
    """Build ChromeOptions with proper headless configuration.
    
    Args:
        headless: If True, run Chrome in headless mode (no visible window)
        user_data_dir: Optional user data directory path (if provided, cache will be preserved for session persistence)
        window_size: Window size string like '1920,1080'
        incognito: If True, run Chrome in incognito/private mode (default True for backward compatibility)
                   Note: When user_data_dir is provided, incognito is automatically disabled for session persistence
    
    Returns:
        uc.ChromeOptions: Configured ChromeOptions object
    """
    if uc is None:
        raise ImportError("undetected_chromedriver is not installed")
    
    opts = uc.ChromeOptions()
    
    # When user_data_dir is provided, we need to preserve cache and disable incognito for session persistence
    if user_data_dir:
        incognito = False
    
    # Headless mode configuration - must be set first
    if headless:
        # Primary headless flag (--headless=new is the modern headless mode)
        opts.add_argument('--headless=new')
        opts.add_argument('--disable-gpu')
        opts.add_argument('--remote-debugging-port=0')
        opts.add_argument('--disable-software-rasterizer')
        # Window positioning to move off-screen if window appears
        opts.add_argument('--window-position=-2000,-2000')
        # Additional flags to prevent window visibility
        opts.add_argument('--hide-scrollbars')
        opts.add_argument('--mute-audio')
        opts.add_argument('--disable-background-timer-throttling')
        opts.add_argument('--disable-backgrounding-occluded-windows')
        opts.add_argument('--disable-renderer-backgrounding')
        opts.add_argument('--disable-features=TranslateUI')
        opts.add_argument('--disable-ipc-flooding-protection')
        # Additional aggressive flags to prevent any window visibility
        opts.add_argument('--disable-infobars')
        opts.add_argument('--disable-notifications')
        opts.add_argument('--disable-popup-blocking')
        opts.add_argument('--disable-default-apps')
        opts.add_argument('--disable-session-crashed-bubble')
        opts.add_argument('--disable-component-update')
        opts.add_argument('--disable-sync')
        opts.add_argument('--no-pings')
        opts.add_argument('--disable-hang-monitor')
        opts.add_argument('--disable-prompt-on-repost')
        opts.add_argument('--disable-domain-reliability')
        opts.add_argument('--disable-client-side-phishing-detection')
        opts.add_argument('--disable-component-extensions-with-background-pages')
        opts.add_argument('--force-color-profile=srgb')
        opts.add_argument('--metrics-recording-only')
        opts.add_argument('--disable-breakpad')
    else:
        opts.add_argument('--disable-gpu')
    
    # Common options for both headless and non-headless
    opts.add_argument('--no-sandbox')
    opts.add_argument('--disable-dev-shm-usage')
    opts.add_argument('--disable-blink-features=AutomationControlled')
    
    if incognito and not user_data_dir:
        opts.add_argument('--incognito')
    
    opts.add_argument(f'--window-size={window_size}')
    
    # Only disable cache when user_data_dir is NOT provided (to allow session persistence)
    if not user_data_dir:
        opts.add_argument('--disable-cache')
        opts.add_argument('--disable-application-cache')
        opts.add_argument('--disk-cache-size=0')
    
    opts.add_argument('--no-first-run')
    opts.add_argument('--no-default-browser-check')
    opts.add_argument('--disable-extensions')
    opts.add_argument('--disable-plugins')

    # Prefer an existing Chrome binary if present
    for path in _detect_chrome_binary_paths():
        opts.binary_location = path
        break

    if user_data_dir:
        opts.add_argument('--user-data-dir=' + user_data_dir)

    return opts


# ============================================================================
# Chrome Driver Creation
# ============================================================================

def get_uc_driver(
    headless: bool = True,
    user_data_dir: Optional[str] = None,
    prefs: Optional[Dict[str, Any]] = None,
    window_size: str = '1366,768',
    retries: int = 2,
    logger: Optional[logging.Logger] = None,
    incognito: bool = True
):
    """Get undetected Chrome driver with automatic version matching and retry logic.
    
    Creates fresh ChromeOptions for each attempt to avoid reuse errors.
    Automatically matches ChromeDriver version to installed Chrome version.
    
    Args:
        headless: If True, run Chrome in headless mode (no visible window)
        user_data_dir: Optional user data directory path
        prefs: Dictionary of Chrome preferences (download settings, etc.)
        window_size: Window size string like '1920,1080'
        retries: Number of retry attempts (unused, kept for backward compatibility)
        logger: Logger instance for logging messages
        incognito: If True, run Chrome in incognito/private mode (default True for backward compatibility)
    
    Returns:
        uc.Chrome: Configured Chrome driver instance
    
    Raises:
        Exception: If all attempts to create driver fail
    """
    if uc is None:
        raise ImportError("undetected_chromedriver is not installed")
    
    # Set environment variable to force headless mode
    if headless:
        os.environ['CHROME_HEADLESS'] = '1'
        os.environ['DISPLAY'] = ':99'  # For Linux systems
    
    if logger:
        logger.info("🚀 Starting browser setup process...")
    
    setup_start_time = time.time()
    major = get_installed_chrome_major_version()
    attempt = 0
    last_err = None
    connection_error_detected = False
    
    if logger:
        if major:
            logger.info(f"🔍 Detected Chrome major version: {major}")
        else:
            logger.warning("⚠️ Could not detect Chrome version, will use auto-detection")
        logger.info(f"🔧 Headless mode: {'ENABLED' if headless else 'DISABLED'}")
        logger.info(f"🔧 Incognito mode: {'ENABLED' if incognito else 'DISABLED'}")
        logger.info(f"🔧 Window size: {window_size}")
        if user_data_dir:
            logger.info(f"🔧 User data directory: {user_data_dir}")
        else:
            logger.info("🔧 User data directory: None (using temporary profile)")
    
    if logger and headless:
        logger.info("📌 Chrome will run in headless mode (no visible browser window)")

    # Detect Chrome binary path
    chrome_binaries = _detect_chrome_binary_paths()
    if logger:
        if chrome_binaries:
            logger.info(f"🔍 Chrome binary found: {chrome_binaries[0]}")
        else:
            logger.warning("⚠️ Chrome binary not found in standard locations")

    # Ensure cache directory exists before attempting driver creation
    cache_dir = ensure_uc_cache_directory()
    if logger and cache_dir:
        logger.info(f"✅ Cache directory ensured: {cache_dir}")
    elif logger:
        logger.warning("⚠️ Could not ensure cache directory exists, continuing anyway")

    # Build version ring: prefer exact match, then try -1, never try +1
    ring = []
    if major:
        ring.append(major)
        if major > 1:
            ring.append(major - 1)
    else:
        ring.append(None)

    # Track if we've seen version mismatch errors (driver too new for Chrome)
    version_mismatch_detected = False
    detected_chrome_version = None
    
    for vm in ring:
        attempt += 1
        try:
            if logger:
                logger.info(f"🧩 Starting Chrome (attempt {attempt}) with version_main={vm}")
                attempt_start_time = time.time()
            
            if logger:
                logger.info("🔧 Building Chrome options...")
            opts = build_default_options(headless=headless, user_data_dir=user_data_dir, window_size=window_size, incognito=incognito)
            if prefs:
                if logger:
                    logger.info("🔧 Adding download preferences...")
                opts.add_experimental_option('prefs', prefs)
            
            if logger:
                logger.info("⚡ Creating Chrome driver instance...")
            driver_start_time = time.time()
            if vm:
                driver = uc.Chrome(options=opts, use_subprocess=True, version_main=vm)
            else:
                driver = uc.Chrome(options=opts, use_subprocess=True)
            driver_duration = time.time() - driver_start_time
            
            if logger:
                logger.info(f"⏱️ Driver creation took {driver_duration:.2f} seconds")
            
            if cache_dir and not validate_driver_file(cache_dir, logger):
                if logger:
                    logger.warning("⚠️ Driver file validation failed, but driver was created. Continuing...")
            
            if logger:
                logger.info("🔍 Validating driver session...")
            if not validate_driver_session(driver, logger):
                connection_error_detected = True
                try:
                    driver.quit()
                except Exception:
                    pass
                raise Exception("Driver session validation failed: connection refused or session lost")
            
            # Get browser capabilities
            try:
                if logger:
                    caps = driver.capabilities
                    browser_version = caps.get('browserVersion', 'Unknown')
                    chrome_version = caps.get('chrome', {}).get('chromedriverVersion', 'Unknown')
                    logger.info(f"🔍 Browser version: {browser_version}")
                    logger.info(f"🔍 ChromeDriver version: {chrome_version}")
            except Exception as e:
                if logger:
                    logger.debug(f"Could not get browser capabilities: {e}")
            
            setup_duration = time.time() - setup_start_time
            if logger:
                logger.info(f"✅ Chrome driver created and session validated successfully")
                logger.info(f"⏱️ Total browser setup time: {setup_duration:.2f} seconds")
            return driver
            
        except Exception as e:
            last_err = e
            error_msg = str(e)
            
            if "Connection refused" in error_msg or "Failed to establish" in error_msg or "Connection refused" in str(type(e).__name__):
                connection_error_detected = True
                if logger:
                    logger.warning(f"⚠️ Connection error detected (attempt {attempt}): {error_msg}")
            
            if "only supports Chrome version" in error_msg and "Current browser version" in error_msg:
                version_mismatch_detected = True
                browser_version_match = re.search(r'Current browser version is (\d+)\.', error_msg)
                if browser_version_match:
                    detected_chrome_version = int(browser_version_match.group(1))
                    if logger:
                        logger.warning(f"⚠️ Version mismatch detected: Driver too new. Chrome is version {detected_chrome_version}")
                
                if logger:
                    logger.warning(f"⚠️ Version mismatch error: {error_msg}")
                
                if detected_chrome_version and detected_chrome_version != vm:
                    if logger:
                        logger.info(f"🔁 Breaking out of version ring to try detected Chrome version: {detected_chrome_version}")
                    break
            
            if "No such file or directory" in error_msg or "Unable to obtain driver" in error_msg:
                if logger:
                    logger.warning(f"⚠️ Driver download/file error (attempt {attempt}): {error_msg}")
            
            if logger:
                logger.warning(f"Chrome launch failed (version_main={vm}): {e}")
            
            if version_mismatch_detected or connection_error_detected:
                if logger:
                    logger.info("🧹 Clearing cache due to version mismatch or connection error...")
                clear_uc_cache()
                time.sleep(5)
            else:
                time.sleep(2)
    
    # If we detected version mismatch, try the detected Chrome version immediately
    if version_mismatch_detected and detected_chrome_version:
        try:
            if logger:
                logger.info(f"🔁 Trying detected Chrome version (version_main={detected_chrome_version}) due to version mismatch")
                attempt_start_time = time.time()
            
            if logger:
                logger.info("🔧 Building Chrome options for detected version...")
            opts = build_default_options(headless=headless, user_data_dir=user_data_dir, window_size=window_size, incognito=incognito)
            if prefs:
                opts.add_experimental_option('prefs', prefs)
            
            if logger:
                logger.info("🧹 Clearing cache before retry...")
            clear_uc_cache()
            time.sleep(5)
            
            if logger:
                logger.info("⚡ Creating Chrome driver with detected version...")
            driver_start_time = time.time()
            driver = uc.Chrome(options=opts, use_subprocess=True, version_main=detected_chrome_version)
            driver_duration = time.time() - driver_start_time
            if logger:
                logger.info(f"⏱️ Driver creation took {driver_duration:.2f} seconds")
            
            if logger:
                logger.info("🔍 Validating driver session...")
            if not validate_driver_session(driver, logger):
                connection_error_detected = True
                try:
                    driver.quit()
                except Exception:
                    pass
                raise Exception("Driver session validation failed after version match")
            
            setup_duration = time.time() - setup_start_time
            if logger:
                logger.info("✅ Chrome driver created with detected version and session validated")
                logger.info(f"⏱️ Total browser setup time: {setup_duration:.2f} seconds")
            return driver
            
        except Exception as e:
            if logger:
                logger.warning(f"Chrome launch failed with version_main={detected_chrome_version}: {e}")
            last_err = e
            error_msg = str(e)
            if "Connection refused" in error_msg or "Failed to establish" in error_msg:
                connection_error_detected = True
    
    # If we detected version mismatch but couldn't extract version, try major-1 and major-2
    if version_mismatch_detected and major and major > 2:
        for fallback_version in [major - 1, major - 2]:
            if fallback_version <= 0:
                continue
            try:
                if logger:
                    logger.info(f"🔁 Trying older ChromeDriver version (version_main={fallback_version}) due to version mismatch")
                    attempt_start_time = time.time()
                
                if logger:
                    logger.info("🔧 Building Chrome options for fallback version...")
                opts = build_default_options(headless=headless, user_data_dir=user_data_dir, window_size=window_size, incognito=incognito)
                if prefs:
                    opts.add_experimental_option('prefs', prefs)
                
                if logger:
                    logger.info("🧹 Clearing cache before fallback attempt...")
                clear_uc_cache()
                time.sleep(5)
                
                if logger:
                    logger.info("⚡ Creating Chrome driver with fallback version...")
                driver_start_time = time.time()
                driver = uc.Chrome(options=opts, use_subprocess=True, version_main=fallback_version)
                driver_duration = time.time() - driver_start_time
                if logger:
                    logger.info(f"⏱️ Driver creation took {driver_duration:.2f} seconds")
                
                if logger:
                    logger.info("🔍 Validating driver session...")
                if not validate_driver_session(driver, logger):
                    connection_error_detected = True
                    try:
                        driver.quit()
                    except Exception:
                        pass
                    raise Exception("Driver session validation failed with fallback version")
                
                setup_duration = time.time() - setup_start_time
                if logger:
                    logger.info("✅ Chrome driver created with fallback version and session validated")
                    logger.info(f"⏱️ Total browser setup time: {setup_duration:.2f} seconds")
                return driver
                
            except Exception as e:
                if logger:
                    logger.warning(f"Chrome launch failed with version_main={fallback_version}: {e}")
                last_err = e
                error_msg = str(e)
                if "Connection refused" in error_msg or "Failed to establish" in error_msg:
                    connection_error_detected = True

    # Final fallback: only use auto-detection if we haven't detected a version mismatch
    if not version_mismatch_detected:
        try:
            if logger:
                logger.info('🔁 Final fallback: launching uc.Chrome() with auto-version detection')
                attempt_start_time = time.time()
            
            if logger:
                logger.info("🔧 Building Chrome options for auto-detection...")
            opts = build_default_options(headless=headless, user_data_dir=user_data_dir, window_size=window_size, incognito=incognito)
            if prefs:
                opts.add_experimental_option('prefs', prefs)
            
            if connection_error_detected:
                if logger:
                    logger.info("🧹 Clearing cache before final fallback attempt...")
                clear_uc_cache()
                time.sleep(5)
            
            if logger:
                logger.info("⚡ Creating Chrome driver with auto-version detection...")
            driver_start_time = time.time()
            driver = uc.Chrome(options=opts, use_subprocess=True)
            driver_duration = time.time() - driver_start_time
            if logger:
                logger.info(f"⏱️ Driver creation took {driver_duration:.2f} seconds")
            
            if logger:
                logger.info("🔍 Validating driver session...")
            if not validate_driver_session(driver, logger):
                connection_error_detected = True
                try:
                    driver.quit()
                except Exception:
                    pass
                raise Exception("Driver session validation failed in final fallback")
            
            setup_duration = time.time() - setup_start_time
            if logger:
                logger.info("✅ Chrome driver created with auto-detection and session validated")
                logger.info(f"⏱️ Total browser setup time: {setup_duration:.2f} seconds")
            return driver
            
        except Exception as e:
            if logger:
                logger.error(f"❌ Final Chrome launch failed: {e}")
            raise last_err or e
    else:
        if logger:
            logger.error(f"❌ All Chrome launch attempts failed after version mismatch detection")
        raise last_err or Exception("Failed to launch Chrome after version mismatch detection")


# ============================================================================
# Download Utilities
# ============================================================================

def create_download_prefs(download_dir: str) -> Dict[str, Any]:
    """Create Chrome download preferences dictionary.
    
    Args:
        download_dir: Directory path for downloads (must be absolute)
        
    Returns:
        dict: Chrome preferences dictionary for downloads
    """
    return {
        "download.default_directory": os.path.abspath(download_dir),
        "download.prompt_for_download": False,
        "download.directory_upgrade": True,
        "plugins.always_open_pdf_externally": True,
        "profile.default_content_setting_values.automatic_downloads": 1,
        "profile.default_content_setting_values.popups": 1,
        "profile.managed_default_content_settings.popups": 1,
        "profile.default_content_settings.popups": 0,
        "profile.content_settings.exceptions.automatic_downloads.*.setting": 1,
        "safebrowsing.enabled": True
    }

