from ws_package.configs import config
from ws_package.utils.system import get_os_type
from ws_package.log.writer import LogWriter
import time
import subprocess
import sqlite3
import os
from pathlib import Path
import re
from typing import List, Dict, Optional
import psutil
import platform

# Linux-specific import
if platform.system() == "Linux":
    try:
        from Xlib import display, X
    except ImportError:
        display = None
        X = None
else:
    display = None
    X = None

# Import printer window detection
from ws_package.monitoring.printer_windows import enhance_window_detection_with_printers

# Try to import AppKit for NSWorkspace (preferred method, non-intrusive)
HAS_APPKIT = False
try:
    from AppKit import NSWorkspace  # type: ignore
    HAS_APPKIT = True
except Exception:
    NSWorkspace = None  # type: ignore
    HAS_APPKIT = False

logger = LogWriter.file(__name__)
# Thread-safe storage for frontmost app (simple cache for lsappinfo polling)
import threading
_frontmost_app_lock = threading.Lock()
_frontmost_app_name = None
_frontmost_app_bundle_id = None

class Window:
    def __init__(self):
        # Initialize Xlib Display only on Linux (macOS/Windows don't use X11)
        if get_os_type() == "Linux":
            try:
                self.d = display.Display()
                self.root = self.d.screen().root
                self.NET_ACTIVE_WINDOW = self.d.intern_atom('_NET_ACTIVE_WINDOW')
                self.NET_WM_NAME = self.d.intern_atom('_NET_WM_NAME')
                self.WM_NAME = self.d.intern_atom('WM_NAME')
                logger.info("Xlib Display initialized (X11 session)")
            except Exception as e:
                # Xlib fails on Wayland sessions and headless environments.
                # App/URL tracking will silently return nothing until Wayland support
                # is implemented. Log prominently so ops can diagnose.
                self.d = None
                wayland = os.environ.get("WAYLAND_DISPLAY") or os.environ.get("XDG_SESSION_TYPE", "")
                if "wayland" in wayland.lower():
                    logger.warning(
                        "[Linux] Wayland session detected — Xlib-based app/URL tracking is NOT "
                        "supported. App and URL activity will not be recorded until Wayland "
                        "support is added. Error: %s", e
                    )
                else:
                    logger.error(
                        "[Linux] Failed to initialize Xlib Display — app/URL tracking disabled. "
                        "Is DISPLAY set? Running headless? Error: %s", e
                    )
        else:
            # macOS/Windows don't use Xlib
            self.d = None
        self.chrome_history_path_mac = os.path.expanduser('~/Library/Application Support/Google/Chrome/Default/History')

        # Temp DB copies — use a stable, user-writable directory instead of './'
        # (cwd can be the PyInstaller _MEIPASS temp dir in frozen builds).
        _tmp_dir = os.path.join(os.path.expanduser("~"), ".workstatus_tmp")
        os.makedirs(_tmp_dir, exist_ok=True)
        self.temp_history_db_path = os.path.join(_tmp_dir, 'Chrome_History_Temp.db')
        
        # Store previous active app to filter out "Ws desktop application"
        self._previous_active_app = None
        self._previous_active_app_path = None

    productive_apps = config("apps", "ProductiveApps")
    unproductive_apps = config("apps", "UnproductiveApps")

    def classify_productivity(self, app_name):
        self.productive_apps = self.productive_apps.split(',')
        self.unproductive_apps = self.unproductive_apps.split(',')
        app = str(app_name.stdout.strip('\n'))
        if app in self.productive_apps:
            return 'Productive'
        elif app in self.unproductive_apps:
            return 'Unproductive'
        else:
            return 'Neutral'

    def get_active_windows(self, fetch_browser_url: bool = True, include_printers: bool = True):
        # Get original active windows
        # Suppress verbose logging for frequent checks (every 10 seconds)
        if get_os_type() == "Windows":
            original_windows = self.get_active_windows_ms(fetch_browser_url=fetch_browser_url)
        elif get_os_type() == "Darwin":
            original_windows = self.get_active_windows_mac()
        elif get_os_type() == "Linux":
            original_windows = self.get_active_windows_linux()
        else:
            original_windows = []
        
        # Enhance with printer window detection (optional — skip for frequent UI polls)
        enhanced_windows = enhance_window_detection_with_printers(
            original_windows,
            enabled=include_printers,
        )
        
        # Ensure all windows have app_path and app_display_name fields,
        # and filter out the Workstatus desktop app itself.
        # Previously `filtered_windows` was built but never returned — the method
        # returned `enhanced_windows` (unfiltered). Fixed to return `filtered_windows`.
        filtered_windows = []
        ignored_apps = {"Ws Desktop App", "workstatus-desktop"}

        for window in enhanced_windows:
            if "app_path" not in window:
                window["app_path"] = None
            if "app_display_name" not in window:
                window["app_display_name"] = window.get("active_app", window.get("module", "Unknown"))

            current_app = window.get("active_app", "")
            should_ignore = any(
                ignored.lower() == current_app.lower()
                for ignored in ignored_apps
                if current_app
            )

            if should_ignore:
                if self._previous_active_app:
                    # Substitute the previous valid app so the record isn't blank
                    window["active_app"] = self._previous_active_app
                    window["module"] = self._previous_active_app
                    window["app_display_name"] = self._previous_active_app
                    window["url"] = self._previous_active_app
                    window["app_path"] = self._previous_active_app_path
                    logger.debug("Ignored '%s', using previous: '%s'", current_app, self._previous_active_app)
                    filtered_windows.append(window)
                else:
                    # No previous app known yet — skip this record entirely
                    logger.debug("Ignored '%s', no previous app — skipping record", current_app)
            else:
                # Valid app: record it and update previous-app cache
                if current_app and current_app != "None":
                    self._previous_active_app = current_app
                    self._previous_active_app_path = window.get("app_path")
                filtered_windows.append(window)

        return filtered_windows

    def get_active_windows_ms(self, fetch_browser_url: bool = True):
        try:
            import win32gui, psutil, time, win32process, win32api
            browsers = [
                'Google Chrome', 'Mozilla Firefox', 'Safari',
                'Microsoft Edge', 'Opera', 'Msedge',
                'Chrome', 'Firefox', 'Brave Browser'
            ]
            # Map display name (or substring) to browser DB key for URL capture (Windows only)
            _browser_title_to_key = {
                'google chrome': 'chrome', 'chrome': 'chrome',
                'mozilla firefox': 'firefox', 'firefox': 'firefox',
                'microsoft edge': 'msedge', 'msedge': 'msedge',
                'brave browser': 'brave', 'opera': 'opera', 'safari': 'safari',
            }

            # Get window handle — returns NULL (0) when no window has focus (screen lock, desktop)
            hwnd = win32gui.GetForegroundWindow()
            if not hwnd:
                logger.debug("[Windows] No foreground window (screen locked or desktop active)")
                return []

            # Get window title
            window_title = win32gui.GetWindowText(hwnd)

            # Get process ID and process early (needed for both browser and non-browser paths)
            _, pid = win32process.GetWindowThreadProcessId(hwnd)
            if pid == 0:
                # PID 0 = System Idle Process; psutil.Process(0) raises AccessDenied on Windows
                logger.debug("[Windows] Foreground window PID is 0 (system idle), skipping")
                return []
            process = psutil.Process(pid)
            exe_path = process.exe()
            app_name = process.name().replace(".exe", "").lower()

            # Check if the window title contains any known browser name — try to get actual URL on Windows
            for browser in browsers:
                if browser.lower() in window_title.lower():
                    browser_key = _browser_title_to_key.get(browser.lower(), app_name if app_name in _browser_title_to_key else None)
                    url_value = window_title
                    if browser_key and fetch_browser_url:
                        try:
                            from ws_package.utils.browser_url_detector import get_url_from_browser_database
                            db_url = get_url_from_browser_database(browser_key, window_title)
                            if db_url and db_url.strip():
                                url_value = db_url.strip()
                                logger.debug("[Windows] Browser URL from DB (%s): %s...", browser_key, url_value[:120])
                        except Exception as db_e:
                            logger.debug("[Windows] Browser DB URL failed (%s): %s", browser_key, db_e)
                    friendly_names_browser = {
                        "chrome": "Google Chrome", "msedge": "Microsoft Edge",
                        "firefox": "Mozilla Firefox", "brave": "Brave Browser",
                        "opera": "Opera", "safari": "Safari",
                    }
                    app_name_clean = friendly_names_browser.get(browser_key, window_title) if browser_key else window_title
                    return [{
                        "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                        "active_app": url_value,
                        "module": url_value,
                        "url": url_value,
                        "app_path": exe_path,
                        "app_display_name": app_name_clean
                    }]

            # Non-browser path: use process name; for browsers not matched by title, try DB URL (Windows)
            try:
                app_desc = win32api.GetFileVersionInfo(
                    exe_path, "\\StringFileInfo\\040904b0\\FileDescription"
                )
            except Exception:
                app_desc = None

            friendly_names = {
                "taskmgr": "Task Manager",
                "explorer": "File Explorer",
                "cmd": "Command Prompt",
                "chrome": "Google Chrome",
                "msedge": "Microsoft Edge",
                "firefox": "Mozilla Firefox",
                "winword": "Microsoft Word",
                "excel": "Microsoft Excel",
                "powerpnt": "Microsoft PowerPoint",
                "outlook": "Microsoft Outlook",
                "code": "Visual Studio Code",
                "notepad": "Notepad",
            }

            if app_desc:
                app_name_clean = app_desc
            elif app_name in friendly_names:
                app_name_clean = friendly_names[app_name]
            else:
                app_name_clean = app_name.capitalize()

            if not app_name_clean:
                app_name_clean = "Desktop"

            url_value = app_name_clean
            # Windows: try to get actual URL when foreground app is a browser (by process name)
            _windows_browser_keys = ('chrome', 'msedge', 'firefox', 'brave', 'opera', 'vivaldi', 'chromium')
            if fetch_browser_url and app_name in _windows_browser_keys:
                try:
                    from ws_package.utils.browser_url_detector import get_url_from_browser_database
                    db_url = get_url_from_browser_database(app_name, window_title)
                    if db_url and db_url.strip():
                        url_value = db_url.strip()
                        logger.debug("[Windows] Browser URL from DB (process %s): %s...", app_name, url_value[:120])
                except Exception as db_e:
                    logger.debug("[Windows] Browser DB URL failed (process %s): %s", app_name, db_e)

            log_data = {
                "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                "active_app": url_value,
                "module": url_value,
                "url": url_value,
                "app_path": exe_path,
                "app_display_name": app_name_clean
            }

            return [log_data]

        except Exception as e:
            return [{
                "error": str(e),
                "module": "Unknown",
                "active_app": "Unknown",
                "url": "Unknown"
            }]

    def get_firefox_url_from_database(self):
        """
        Attempt to read the current URL from Firefox's places.sqlite database
        Note: This method may not work if Firefox is running due to database locks
        """
        try:
            import sqlite3
            import os
            from pathlib import Path
            
            # Firefox profile paths for different OS
            if get_os_type() == "Windows":
                firefox_profiles = [
                    os.path.expanduser("~/AppData/Roaming/Mozilla/Firefox/Profiles"),
                    os.path.expanduser("~/AppData/Local/Mozilla/Firefox/Profiles")
                ]
            elif get_os_type() == "Darwin":
                firefox_profiles = [
                    os.path.expanduser("~/Library/Application Support/Firefox/Profiles")
                ]
            else:  # Linux
                firefox_profiles = [
                    os.path.expanduser("~/.mozilla/firefox")
                ]
            
            for profile_path in firefox_profiles:
                if os.path.exists(profile_path):
                    # Find the default profile directory
                    for item in os.listdir(profile_path):
                        if item.endswith('.default') or item.endswith('.default-release'):
                            profile_dir = os.path.join(profile_path, item)
                            places_db = os.path.join(profile_dir, 'places.sqlite')
                            
                            if os.path.exists(places_db):
                                try:
                                    # Use SQLite backup API — handles WAL files; shutil.copy2 causes "malformed" errors
                                    temp_db = os.path.join(os.path.expanduser("~"), ".workstatus_tmp", 'firefox_places_temp.db')
                                    _src = sqlite3.connect(places_db, timeout=5)
                                    try:
                                        _dst = sqlite3.connect(temp_db, timeout=5)
                                        try:
                                            _src.backup(_dst)
                                        finally:
                                            _dst.close()
                                    finally:
                                        _src.close()

                                    # Query the database for the most recent URL
                                    conn = None
                                    try:
                                        conn = sqlite3.connect(temp_db, timeout=5)
                                        cursor = conn.cursor()
                                        cursor.execute("""
                                            SELECT url, title, visit_date
                                            FROM moz_places
                                            JOIN moz_historyvisits ON moz_places.id = moz_historyvisits.place_id
                                            ORDER BY visit_date DESC
                                            LIMIT 1
                                        """)
                                        result = cursor.fetchone()
                                    finally:
                                        if conn:
                                            conn.close()
                                    
                                    # Clean up temporary file
                                    if os.path.exists(temp_db):
                                        os.remove(temp_db)
                                    
                                    if result:
                                        return result[0]  # Return the URL
                                        
                                except sqlite3.OperationalError as e:
                                    logger.info("Database locked or inaccessible: %s", e)
                                    continue
                                except Exception as e:
                                    logger.info("Error reading Firefox database: %s", e)
                                    continue
            
            return None
            
        except Exception as e:
            logger.error("Error accessing Firefox database: %s", e)
            return None
   
    def get_active_windows_mac(self):
        try:
            # Get both browser code AND app name from check_frontmost_app() in ONE call
            # Uses NSWorkspace (no AppleScript) to avoid CPU usage and WindowServer load
            frontmost_app_code, frontmost_app_name = self.check_frontmost_app()
            
            result = None
            if frontmost_app_code == 0:
                result = self.get_active_browser_chrome()
            elif frontmost_app_code == 1:
                result = self.get_active_browser_safari()
            elif frontmost_app_code == 2:
                result = self.get_active_browser_brave()
            elif frontmost_app_code == 3:
                result = self.get_active_browser_firefox()
            
            # Ensure we always return a list, not None
            if result is not None:
                return result
            else:
                # Use the app name we already got from check_frontmost_app()
                # NO additional AppleScript call needed - eliminates focus interruptions!
                active_app_name = frontmost_app_name
                
                if not active_app_name:
                    # check_frontmost_app() failed - try fallback using psutil to get active process
                    logger.warning("[FOCUS_CHECK] No app name from check_frontmost_app - attempting psutil fallback (less accurate)")
                    try:
                        # Get the frontmost process using psutil (fallback when NSWorkspace fails)
                        for proc in psutil.process_iter(['pid', 'name', 'status']):
                            try:
                                # Check if process is running and try to determine if it's frontmost
                                # Note: psutil doesn't directly support "frontmost", but we can get the active window's process
                                # For macOS, we'll use a simple heuristic: get processes with GUI
                                if proc.info['status'] == psutil.STATUS_RUNNING:
                                    proc_name = proc.info['name']
                                    # Filter out system processes and focus on user applications
                                    if proc_name and not proc_name.startswith('kernel') and proc_name not in ['WindowServer', 'Dock', 'Finder']:
                                        # Use the first reasonable GUI process as fallback
                                        # This is a best-effort approach when NSWorkspace fails
                                        active_app_name = proc_name.replace('.app', '').replace('.exe', '')
                                        logger.warning("[FOCUS_CHECK] Using psutil fallback - detected app: '%s' (may be inaccurate)", active_app_name)
                                        break
                            except (psutil.NoSuchProcess, psutil.AccessDenied):
                                continue
                    except Exception as e:
                        logger.error("[FOCUS_CHECK] psutil fallback also failed: %s: %s", type(e).__name__, e)
                    
                    if not active_app_name:
                        # Both NSWorkspace and psutil fallback failed - return empty list gracefully
                        logger.error("[FOCUS_CHECK] Unable to detect active app - both NSWorkspace and psutil fallback failed. This may indicate permission issues.")
                        return []
                
                # Validate active_app_name before processing (must be non-empty string)
                if not active_app_name or not isinstance(active_app_name, str) or not active_app_name.strip():
                    logger.warning("[FOCUS_CHECK] Invalid active_app_name before rename: %s (type: %s)", active_app_name, type(active_app_name))
                    return []
                
                # Rename/normalize the app name
                active_app_name = self.rename(active_app_name)
                
                # Validate after rename (in case rename returns empty/None)
                if not active_app_name or not isinstance(active_app_name, str) or not active_app_name.strip():
                    logger.warning("[FOCUS_CHECK] active_app_name is empty/invalid after rename: %s (type: %s)", active_app_name, type(active_app_name))
                    return []
                
                logger.debug("[Firefox Detection] Fallback: Detected app name as '%s'", active_app_name)
                
                # Use NSWorkspace to get bundle ID (no AppleScript to avoid WindowServer load)
                try:
                    if HAS_APPKIT:
                        workspace = NSWorkspace.sharedWorkspace()
                        front_app = workspace.frontmostApplication()
                        if front_app:
                            bundle_id = str(front_app.bundleIdentifier())
                            if bundle_id and bundle_id.startswith('com.microsoft.VSCode'):
                                active_app_name = 'VS Code'
                except Exception:
                    pass  # Bundle ID lookup is optional, fail silently

                # Final validation before creating log entry (defensive check after bundle ID lookup)
                # This ensures we never create log entries with empty strings
                if not active_app_name or not isinstance(active_app_name, str) or not active_app_name.strip():
                    logger.warning("[FOCUS_CHECK] active_app_name became empty/invalid after bundle ID check: %s (type: %s)", active_app_name, type(active_app_name))
                    return []

                # Try to get app bundle path for icon extraction
                app_path = None
                try:
                    bundle_cmd = """osascript -e 'tell application "System Events" to get POSIX path of (application file of (first process whose frontmost is true))'"""
                    result = subprocess.run(bundle_cmd, shell=True, capture_output=True, text=True, timeout=2)
                    if result.returncode == 0:
                        app_path = result.stdout.strip()
                except Exception:
                    pass  # Bundle path lookup is optional
                
                log_data = [{
                    "timestamp": time.strftime('%Y-%m-%d %H:%M:%S'),
                    "module": active_app_name,
                    "active_app": active_app_name,
                    "url": active_app_name,
                    "app_path": app_path,  # Add app bundle path for icon extraction
                    "app_display_name": active_app_name  # Add display name
                }]
                
                # Return the list of logs
                return log_data

        except subprocess.CalledProcessError as e:
            # Subprocess errors are unexpected now (no AppleScript) - log for visibility
            logger.error("[FOCUS_CHECK] Subprocess error in get_active_windows_mac: returncode=%s, stderr=%s", e.returncode, e.stderr.decode('utf-8') if e.stderr else 'None')
            return []
        except Exception as e:
            logger.error("[FOCUS_CHECK] Unexpected error in get_active_windows_mac: %s: %s", type(e).__name__, e)
            return []

    def check_frontmost_app(self) -> tuple:
        """
        Get the frontmost application and return its browser code AND app name.
        
        Uses lsappinfo polling (reliable from background threads) with NSWorkspace as fallback.
        lsappinfo is called every 10 seconds to detect app changes.
        
        Returns:
            tuple: (browser_code, app_name)
                   browser_code: 0=Chrome, 1=Safari, 2=Brave, 3=Firefox, -1=other/not browser
                   app_name: The actual application name string (or None if detection failed)
        """
        try:
            global _frontmost_app_name, _frontmost_app_lock

            # Primary method: lsappinfo polling (reliable from background threads)
            logger.debug("[FOCUS_CHECK] Polling lsappinfo for fresh app detection...")
            app_name = self._get_frontmost_app_via_lsappinfo()
            if app_name and isinstance(app_name, str):
                # Update cache with fresh value
                with _frontmost_app_lock:
                    old_cached = _frontmost_app_name
                    _frontmost_app_name = app_name
                browser_code = self._map_frontmost_app_to_code(app_name)
                if old_cached != app_name:
                    logger.info("[FOCUS_CHECK] ✅ App changed: '%s' -> '%s' (via lsappinfo, browser_code: %s)", old_cached, app_name, browser_code)
                else:
                    logger.debug("[FOCUS_CHECK] lsappinfo detected same app: '%s' -> browser_code: %s", app_name, browser_code)
                return (browser_code, app_name)

            # Fallback: NSWorkspace (may return stale results from background threads)
            logger.debug("[FOCUS_CHECK] lsappinfo failed, trying NSWorkspace fallback...")
            app_name = self._get_frontmost_app_via_nsworkspace()
            if app_name and isinstance(app_name, str):
                browser_code = self._map_frontmost_app_to_code(app_name)
                logger.info("[FOCUS_CHECK] NSWorkspace fallback detected app: '%s' -> browser_code: %s", app_name, browser_code)
                return (browser_code, app_name)

            logger.warning("[FOCUS_CHECK] No valid app name detected - both lsappinfo and NSWorkspace failed")
            return (-1, None)
        except Exception as e:
            logger.error("[FOCUS_CHECK] Error in check_frontmost_app: %s: %s", type(e).__name__, e, exc_info=True)
            return (-1, None)

    def _map_frontmost_app_to_code(self, app_name: str) -> int:
        """Map a frontmost application name to a browser identifier code."""
        normalized = app_name.strip().lower()
        if normalized in {"google chrome", "chrome"}:
            return 0
        if normalized in {"safari"}:
            return 1
        if normalized in {"brave browser", "brave"}:
            return 2
        if normalized in {"firefox", "mozilla firefox"}:
            return 3
        return -1

    def _get_frontmost_app_via_nsworkspace(self) -> Optional[str]:
        """Fallback method using NSWorkspace (when available) to get the frontmost macOS app.
        
        NOTE: NSWorkspace may return stale/cached results when called from background threads.
        This is why lsappinfo is used as primary method. NSWorkspace is used as fallback only.
        
        IMPORTANT: NSWorkspace.frontmostApplication() may return cached results when called
        from async/background tasks. This method is kept as fallback but lsappinfo is preferred.
        """
        if not HAS_APPKIT:
            logger.debug("[FOCUS_CHECK] NSWorkspace (AppKit) not available - PyObjC may not be installed.")
            return None
        try:
            logger.debug("[FOCUS_CHECK] Attempting NSWorkspace frontmost app detection...")
            
            workspace = NSWorkspace.sharedWorkspace()
            front_app = workspace.frontmostApplication()
            
            if front_app:
                app_name = str(front_app.localizedName())
                
                # Validate that we got a non-empty string
                if app_name and app_name.strip():
                    logger.debug("[FOCUS_CHECK] NSWorkspace detected frontmost app: '%s'", app_name)
                    return app_name
                else:
                    logger.debug("[FOCUS_CHECK] NSWorkspace returned empty string for app name: '%s'", app_name)
            else:
                logger.debug("[FOCUS_CHECK] NSWorkspace returned None for frontmost app")
        except Exception as e:
            logger.warning("[FOCUS_CHECK] NSWorkspace frontmost app lookup failed: %s: %s", type(e).__name__, e)
        return None

    def _normalize_lsappinfo_name(self, raw_name: Optional[str]) -> Optional[str]:
        """Normalize lsappinfo output to a clean app name."""
        if not raw_name:
            return None
        name = raw_name.strip()
        # If format like '"LSDisplayName"="Cursor"' or 'LSDisplayName=Cursor'
        for token in ["LSDisplayName", "LSName", "name"]:
            if token.lower() in name.lower() and "=" in name:
                parts = name.split("=", 1)
                if len(parts) == 2:
                    name = parts[1].strip().strip('"').strip()
                    break
        # Strip surrounding quotes if any remain
        if name.startswith('"') and name.endswith('"') and len(name) >= 2:
            name = name[1:-1]
        return name or None

    def _get_frontmost_app_via_lsappinfo(self) -> Optional[str]:
        """Primary method using lsappinfo (macOS command-line tool, reliable from background threads).
        
        lsappinfo is a native macOS utility that works reliably from background threads
        and doesn't cause WindowServer load. It's more reliable than NSWorkspace when called
        from async/background tasks because it doesn't have caching issues.
        
        Returns:
            Optional[str]: App name if detected, None otherwise
        """
        try:
            logger.debug("[FOCUS_CHECK] Attempting lsappinfo frontmost app detection...")
            
            # Step 1: get the front ASN (Application Serial Number)
            front_cmd = ['lsappinfo', 'front']
            front_res = subprocess.run(front_cmd, capture_output=True, text=True, timeout=1.0)
            if front_res.returncode != 0 or not front_res.stdout.strip():
                logger.warning("[FOCUS_CHECK] lsappinfo front failed (rc=%s, out='%s', err='%s')", front_res.returncode, front_res.stdout.strip(), front_res.stderr.strip() if front_res.stderr else '')
                return None
            
            asn = front_res.stdout.strip()
            if not asn.startswith("ASN:"):
                logger.debug("[FOCUS_CHECK] lsappinfo front returned non-ASN output, skipping: %s", asn)
                return None
            
            # Step 2: resolve name from ASN
            name_cmd = ['lsappinfo', 'info', '-only', 'name', asn]
            name_res = subprocess.run(name_cmd, capture_output=True, text=True, timeout=1.0)
            if name_res.returncode == 0 and name_res.stdout.strip():
                app_name = self._normalize_lsappinfo_name(name_res.stdout.strip())
                if app_name:
                    logger.info("[FOCUS_CHECK] lsappinfo detected app via ASN->name: '%s'", app_name)
                    return app_name
                else:
                    logger.debug("[FOCUS_CHECK] lsappinfo name normalization returned None for: '%s'", name_res.stdout.strip())
            else:
                logger.debug("[FOCUS_CHECK] lsappinfo name lookup failed (rc=%s, out='%s', err='%s')", name_res.returncode, name_res.stdout.strip(), name_res.stderr.strip() if name_res.stderr else '')
            
            # Step 3: resolve bundleid from ASN
            bundle_cmd = ['lsappinfo', 'info', '-only', 'bundleid', asn]
            bundle_res = subprocess.run(bundle_cmd, capture_output=True, text=True, timeout=1.0)
            if bundle_res.returncode == 0 and bundle_res.stdout.strip():
                bundle_id = bundle_res.stdout.strip()
                logger.debug("[FOCUS_CHECK] lsappinfo bundleid lookup returned: '%s'", bundle_id)
                
                if HAS_APPKIT:
                    try:
                        workspace = NSWorkspace.sharedWorkspace()
                        app_url = workspace.URLForApplicationWithBundleIdentifier_(bundle_id)
                        if app_url:
                            app_name = self._normalize_lsappinfo_name(app_url.lastPathComponent().replace('.app', ''))
                            logger.info("[FOCUS_CHECK] lsappinfo detected app via bundle ID (NSWorkspace): '%s'", app_name)
                            return app_name
                    except Exception as e:
                        logger.debug("[FOCUS_CHECK] NSWorkspace bundle ID conversion failed: %s: %s", type(e).__name__, e)
                
                # Fallback: parse bundle ID
                if '.' in bundle_id:
                    app_name = bundle_id.split('.')[-1]
                    app_name = app_name[0].upper() + app_name[1:] if app_name else None
                    if app_name:
                        logger.info("[FOCUS_CHECK] lsappinfo detected app from bundle ID (parsed): '%s'", app_name)
                        return app_name
            else:
                logger.debug("[FOCUS_CHECK] lsappinfo bundleid lookup failed (rc=%s, out='%s', err='%s')", bundle_res.returncode, bundle_res.stdout.strip(), bundle_res.stderr.strip() if bundle_res.stderr else '')
            
            logger.warning("[FOCUS_CHECK] lsappinfo completely failed - all methods (ASN->name, ASN->bundleid->NSWorkspace, ASN->bundleid->parse) returned None")
            return None
        except subprocess.TimeoutExpired:
            logger.warning("[FOCUS_CHECK] lsappinfo timeout after 1.0s")
            return None
        except FileNotFoundError:
            logger.warning("[FOCUS_CHECK] lsappinfo not available on this system")
            return None
        except Exception as e:
            logger.error("[FOCUS_CHECK] lsappinfo unexpected error: %s: %s", type(e).__name__, e, exc_info=True)
            return None

    def get_active_browser_chrome(self):
        """Get Chrome URL from database with retry logic.
        
        Uses browser database access only (no AppleScript) to avoid CPU/WindowServer load.
        Returns None if database access fails.
        """
        # Try database access first (with retry for database locks)
        max_retries = 3
        retry_delay = 0.1  # Start with 100ms
        
        for attempt in range(max_retries):
            try:
                from ws_package.utils.browser_url_detector import get_url_from_browser_database
                
                # Use lowercase "chrome" to match get_browser_db_paths() keys
                url = get_url_from_browser_database("chrome")
                if url and url.strip():
                    log_data = [{
                        "timestamp": time.strftime('%Y-%m-%d %H:%M:%S'),
                        "module": url,
                        "active_app": url,
                        "url": url
                    }]
                    logger.debug("[FOCUS_CHECK] Chrome URL from database: %s", url)
                    return log_data
                else:
                    # No URL found in database - return None (no AppleScript fallback)
                    logger.debug("[FOCUS_CHECK] No Chrome URL found in database, returning None (AppleScript removed)")
                    break
            except ImportError as e:
                logger.warning("[FOCUS_CHECK] Browser database module not available: %s", e)
                break  # Return None (no AppleScript fallback)
            except Exception as e:
                error_msg = str(e).lower()
                if "database is locked" in error_msg or "locked" in error_msg:
                    if attempt < max_retries - 1:
                        wait_time = retry_delay * (2 ** attempt)  # Exponential backoff
                        logger.warning("[FOCUS_CHECK] Chrome database locked, retrying in %ss (attempt %s/%s)", wait_time, attempt + 1, max_retries)
                        time.sleep(wait_time)
                        continue
                    else:
                        logger.warning("[FOCUS_CHECK] Chrome database locked after %s attempts, returning None (AppleScript removed)", max_retries)
                        break  # Return None (no AppleScript fallback)
                else:
                    logger.error("[FOCUS_CHECK] Chrome database error: %s: %s", type(e).__name__, e)
                    break  # Return None (no AppleScript fallback)
        
        # Return None if database access failed (no AppleScript fallback)
        logger.debug("[FOCUS_CHECK] Chrome database access failed, returning None (AppleScript removed)")
        return None

    def get_active_browser_safari(self):
        """Get the most recently visited Safari URL from History.db.

        Safari's History.db uses a different schema to Chrome (history_items +
        history_visits tables; CoreData epoch timestamps).  We delegate to
        get_safari_url_from_db() which handles the backup-copy approach needed
        when Safari holds an exclusive WAL lock.

        Returns a one-item list in the same format as get_active_browser_chrome(),
        or None when the DB is locked / unreadable (caller falls back to app name).
        """
        max_retries = 3
        retry_delay = 0.1  # 100 ms, doubled each retry

        for attempt in range(max_retries):
            try:
                from ws_package.utils.browser_url_detector import get_url_from_browser_database

                url = get_url_from_browser_database("safari")
                if url and url.strip():
                    log_data = [{
                        "timestamp": time.strftime('%Y-%m-%d %H:%M:%S'),
                        "module": url,
                        "active_app": url,
                        "url": url,
                    }]
                    logger.debug("[FOCUS_CHECK] Safari URL from History.db: %s", url)
                    return log_data
                else:
                    logger.debug("[FOCUS_CHECK] Safari History.db returned no URL (may be locked or empty)")
                    return None  # Caller falls back to app name "Safari"

            except ImportError as e:
                logger.warning("[FOCUS_CHECK] Browser database module not available: %s", e)
                return None
            except Exception as e:
                err = str(e).lower()
                if ("locked" in err or "busy" in err) and attempt < max_retries - 1:
                    wait = retry_delay * (2 ** attempt)
                    logger.debug("[FOCUS_CHECK] Safari DB locked, retrying in %.2fs (attempt %s/%s)",
                                 wait, attempt + 1, max_retries)
                    time.sleep(wait)
                    continue
                logger.debug("[FOCUS_CHECK] Safari DB error (%s): %s", type(e).__name__, e)
                return None

        return None

    def get_active_browser_brave(self):
        """Get Brave URL from database with retry logic.
        
        Uses browser database access only (no AppleScript) to avoid CPU/WindowServer load.
        Returns None if database access fails.
        """
        # Try database access first (with retry for database locks)
        max_retries = 3
        retry_delay = 0.1  # Start with 100ms
        
        for attempt in range(max_retries):
            try:
                from ws_package.utils.browser_url_detector import get_url_from_browser_database
                
                # Use lowercase "brave" to match get_browser_db_paths() keys
                url = get_url_from_browser_database("brave")
                if url and url.strip():
                    log_data = [{
                        "timestamp": time.strftime('%Y-%m-%d %H:%M:%S'),
                        "module": url,
                        "active_app": url,
                        "url": url
                    }]
                    logger.debug("[FOCUS_CHECK] Brave URL from database: %s", url)
                    return log_data
                else:
                    # No URL found in database - return None (no AppleScript fallback)
                    logger.debug("[FOCUS_CHECK] No Brave URL found in database, returning None (AppleScript removed)")
                    break
            except ImportError as e:
                logger.warning("[FOCUS_CHECK] Browser database module not available: %s", e)
                break  # Return None (no AppleScript fallback)
            except Exception as e:
                error_msg = str(e).lower()
                if "database is locked" in error_msg or "locked" in error_msg:
                    if attempt < max_retries - 1:
                        wait_time = retry_delay * (2 ** attempt)  # Exponential backoff
                        logger.warning("[FOCUS_CHECK] Brave database locked, retrying in %ss (attempt %s/%s)", wait_time, attempt + 1, max_retries)
                        time.sleep(wait_time)
                        continue
                    else:
                        logger.warning("[FOCUS_CHECK] Brave database locked after %s attempts, returning None (AppleScript removed)", max_retries)
                        break  # Return None (no AppleScript fallback)
                else:
                    logger.error("[FOCUS_CHECK] Brave database error: %s: %s", type(e).__name__, e)
                    break  # Return None (no AppleScript fallback)
        
        # Return None if database access failed (no AppleScript fallback)
        logger.debug("[FOCUS_CHECK] Brave database access failed, returning None (AppleScript removed to avoid WindowServer load)")
        return None

    def get_active_browser_firefox(self):
        """Get Firefox URL from database with retry logic.
        
        Uses browser database access only (no AppleScript) to avoid CPU/WindowServer load.
        Returns None if database access fails.
        """
        # Try database access first (with retry for database locks)
        max_retries = 3
        retry_delay = 0.1  # Start with 100ms
        
        for attempt in range(max_retries):
            try:
                from ws_package.utils.browser_url_detector import get_url_from_browser_database
                
                # Use lowercase "firefox" to match get_browser_db_paths() keys
                url = get_url_from_browser_database("firefox")
                if url and url.strip():
                    log_data = [{
                        "timestamp": time.strftime('%Y-%m-%d %H:%M:%S'),
                        "module": url,
                        "active_app": url,
                        "url": url
                    }]
                    logger.debug("[FOCUS_CHECK] Firefox URL from database: %s", url)
                    return log_data
                else:
                    # No URL found in database - return None (no AppleScript fallback)
                    logger.debug("[FOCUS_CHECK] No Firefox URL found in database, returning None (AppleScript removed)")
                    break
            except ImportError as e:
                logger.warning("[FOCUS_CHECK] Browser database module not available: %s", e)
                break  # Return None (no AppleScript fallback)
            except Exception as e:
                error_msg = str(e).lower()
                if "database is locked" in error_msg or "locked" in error_msg:
                    if attempt < max_retries - 1:
                        wait_time = retry_delay * (2 ** attempt)  # Exponential backoff
                        logger.warning("[FOCUS_CHECK] Firefox database locked, retrying in %ss (attempt %s/%s)", wait_time, attempt + 1, max_retries)
                        time.sleep(wait_time)
                        continue
                    else:
                        logger.warning("[FOCUS_CHECK] Firefox database locked after %s attempts, returning None (AppleScript removed)", max_retries)
                        break  # Return None (no AppleScript fallback)
                else:
                    logger.error("[FOCUS_CHECK] Firefox database error: %s: %s", type(e).__name__, e)
                    break  # Return None (no AppleScript fallback)
        
        # Return None if database access failed (no AppleScript fallback)
        logger.debug("[FOCUS_CHECK] Firefox database access failed, returning None (AppleScript removed to avoid WindowServer load)")
        return None

    def rename(self, window_name):
        """
        Rename/normalize window name.
        
        Args:
            window_name: The window/app name to normalize (str, None, or empty)
        
        Returns:
            str: Normalized window name, or empty string if input is invalid
        """
        # Handle None/empty input gracefully
        if not window_name or not isinstance(window_name, str):
            return ""
        
        # Remove any trailing digits or brackets like [1], [2], etc.
        cleaned_name = re.sub(r'\s*\[.*?\]$', '', window_name)  # Remove brackets with content
        cleaned_name = cleaned_name.strip()  # Remove extra spaces
        
        # Return empty string if result is empty after cleaning
        if not cleaned_name:
            return ""

        # Normalize common Electron/VS Code naming to show as "VS Code"
        vscode_names = {
            "Code",
            "Code Helper",
            "Code Helper (Renderer)",
            "Code Helper (GPU)",
            "Visual Studio Code",
            "Code - Insiders",
        }
        if cleaned_name in vscode_names:
            return "VS Code"
        
        return cleaned_name

    # ------------------------------------------------------------------
    # Wayland active-window detection
    # ------------------------------------------------------------------
    # Tried in order: Hyprland → Sway/i3 → GNOME Shell (gdbus) → KDE (qdbus)
    # → xdotool (XWayland apps).  First success wins; all failures → None.
    # Logged once per session (not every poll) to avoid log spam.
    _wayland_warn_logged: bool = False

    def _get_active_window_wayland(self):
        """Return (app_name, window_title) for the focused window on Wayland.

        Tries compositor-specific tools in order of prevalence:
          1. Hyprland  — hyprctl activewindow -j
          2. Sway/i3   — swaymsg -t get_tree   (finds focused leaf)
          3. GNOME     — gdbus call → org.gnome.Shell.Eval
          4. KDE/KWin  — qdbus org.kde.KWin /KWin queryWindowInfo
          5. xdotool   — works for XWayland apps on any compositor

        Returns (app_name: str, title: str) or (None, None) on total failure.
        """
        import json as _json

        # ── 1. Hyprland ───────────────────────────────────────────────
        try:
            r = subprocess.run(
                ["hyprctl", "activewindow", "-j"],
                capture_output=True, text=True, timeout=2,
            )
            if r.returncode == 0 and r.stdout.strip():
                data = _json.loads(r.stdout)
                app = data.get("class") or data.get("initialClass") or ""
                title = data.get("title") or app
                if app:
                    logger.debug("[Wayland/Hyprland] active window: class=%s title=%s", app, title)
                    return app, title
        except (FileNotFoundError, _json.JSONDecodeError, subprocess.TimeoutExpired):
            pass
        except Exception as e:
            logger.debug("[Wayland/Hyprland] error: %s", e)

        # ── 2. Sway / i3 ──────────────────────────────────────────────
        try:
            r = subprocess.run(
                ["swaymsg", "-t", "get_tree"],
                capture_output=True, text=True, timeout=2,
            )
            if r.returncode == 0 and r.stdout.strip():
                def _find_focused(node):
                    if node.get("focused"):
                        return node
                    for child in node.get("nodes", []) + node.get("floating_nodes", []):
                        result = _find_focused(child)
                        if result:
                            return result
                    return None
                tree = _json.loads(r.stdout)
                focused = _find_focused(tree)
                if focused:
                    app = (focused.get("app_id") or
                           (focused.get("window_properties") or {}).get("class") or "")
                    title = focused.get("name") or app
                    if app:
                        logger.debug("[Wayland/Sway] active window: app=%s title=%s", app, title)
                        return app, title
        except (FileNotFoundError, _json.JSONDecodeError, subprocess.TimeoutExpired):
            pass
        except Exception as e:
            logger.debug("[Wayland/Sway] error: %s", e)

        # ── 3. GNOME Shell (gdbus) ─────────────────────────────────────
        try:
            r = subprocess.run([
                "gdbus", "call", "--session",
                "--dest", "org.gnome.Shell",
                "--object-path", "/org/gnome/Shell",
                "--method", "org.gnome.Shell.Eval",
                "(function(){"
                "var w=global.display.get_focus_window();"
                "if(!w)return JSON.stringify({});"
                "return JSON.stringify({"
                "  app:w.get_wm_class(),"
                "  title:w.get_title()"
                "});"
                "})()",
            ], capture_output=True, text=True, timeout=3)
            if r.returncode == 0:
                # gdbus output: (true, '"{\\"app\\":\\"Firefox\\",\\"title\\":\\"...\\"}"\n')
                out = r.stdout.strip()
                # Extract the inner JSON string
                inner = re.search(r'"(\{.*\})"', out)
                if inner:
                    data = _json.loads(inner.group(1).replace('\\"', '"'))
                    app = data.get("app") or ""
                    title = data.get("title") or app
                    if app:
                        logger.debug("[Wayland/GNOME] active window: app=%s title=%s", app, title)
                        return app, title
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass
        except Exception as e:
            logger.debug("[Wayland/GNOME] gdbus error: %s", e)

        # ── 4. KDE / KWin (qdbus) ─────────────────────────────────────
        try:
            # activeWindow() returns the window ID as a hex string
            r = subprocess.run(
                ["qdbus", "org.kde.KWin", "/KWin", "activeWindow"],
                capture_output=True, text=True, timeout=2,
            )
            if r.returncode == 0 and r.stdout.strip():
                win_id = r.stdout.strip()
                # queryWindowInfo takes the window ID
                r2 = subprocess.run(
                    ["qdbus", "org.kde.KWin", "/KWin", "queryWindowInfo", win_id],
                    capture_output=True, text=True, timeout=2,
                )
                if r2.returncode == 0 and r2.stdout.strip():
                    # Parse key: value pairs from qdbus output
                    info = {}
                    for line in r2.stdout.splitlines():
                        if ":" in line:
                            k, _, v = line.partition(":")
                            info[k.strip()] = v.strip()
                    app = info.get("resourceClass") or info.get("resourceName") or ""
                    title = info.get("caption") or app
                    if app:
                        logger.debug("[Wayland/KWin] active window: app=%s title=%s", app, title)
                        return app, title
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass
        except Exception as e:
            logger.debug("[Wayland/KWin] qdbus error: %s", e)

        # ── 5. xdotool (XWayland apps on any compositor) ──────────────
        try:
            r = subprocess.run(
                ["xdotool", "getactivewindow", "getwindowname"],
                capture_output=True, text=True, timeout=2,
            )
            if r.returncode == 0 and r.stdout.strip():
                title = r.stdout.strip()
                # Derive app name from title (last segment after " – " or " - ")
                app = re.split(r"\s[–-]\s", title)[-1].strip() or title
                logger.debug("[Wayland/xdotool] active window: title=%s", title)
                return app, title
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass
        except Exception as e:
            logger.debug("[Wayland/xdotool] error: %s", e)

        # All tools failed
        if not Window._wayland_warn_logged:
            Window._wayland_warn_logged = True
            logger.warning(
                "[Wayland] No compositor tool found to detect active window. "
                "Install one of: hyprctl (Hyprland), swaymsg (Sway), "
                "gdbus/gnome-shell (GNOME), qdbus (KDE), xdotool (XWayland). "
                "App/URL tracking will be empty until a supported tool is available."
            )
        return None, None

    def get_active_windows_linux(self):
        if not self.d or X is None:
            # X11 unavailable — try Wayland compositor tools
            app_name, title = self._get_active_window_wayland()
            if not app_name:
                return []
            # Apply same friendly-name mapping as the X11 path
            friendly_names = {
                "code": "Visual Studio Code", "firefox": "Firefox",
                "google-chrome": "Google Chrome", "chromium": "Chromium",
                "brave-browser": "Brave Browser", "brave": "Brave Browser",
                "gnome-terminal": "Terminal", "konsole": "Konsole",
                "xterm": "Terminal", "nautilus": "File Manager",
                "dolphin": "File Manager", "thunar": "File Manager",
                "gedit": "Text Editor", "vim": "Vim", "emacs": "Emacs",
                "libreoffice": "LibreOffice", "gimp": "GIMP",
                "slack": "Slack", "discord": "Discord",
                "telegram-desktop": "Telegram", "zoom": "Zoom",
                "vlc": "VLC Media Player", "spotify": "Spotify",
                "thunderbird": "Thunderbird",
            }
            app_clean = app_name.lower().replace("-", "_")
            display_name = (
                friendly_names.get(app_name.lower()) or
                friendly_names.get(app_clean) or
                app_name
            )
            return [{
                "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                "module": display_name,
                "active_app": display_name,
                "url": display_name,
            }]

        try:
            window_id = self.root.get_full_property(self.NET_ACTIVE_WINDOW, X.AnyPropertyType).value[0]
            window = self.d.create_resource_object('window', window_id)

            # Get the process ID (PID) of the window
            pid_prop = window.get_full_property(self.d.intern_atom('_NET_WM_PID'), X.AnyPropertyType)
            
            app_name = None
            
            if pid_prop and pid_prop.value:
                pid = pid_prop.value[0]
                try:
                    # Use psutil to get process info
                    process = psutil.Process(pid)
                    app_name = process.name()
                    
                    # Clean up the app name
                    app_name_clean = app_name.replace(".exe", "").strip().lower()
                    
                    # Friendly name mapping for common Linux applications
                    friendly_names = {
                        "code": "Visual Studio Code",
                        "firefox": "Firefox",
                        "google-chrome": "Google Chrome",
                        "chromium": "Chromium",
                        "brave": "Brave Browser",
                        "gnome-terminal": "Terminal",
                        "konsole": "Konsole",
                        "xterm": "Terminal",
                        "nautilus": "File Manager",
                        "dolphin": "File Manager",
                        "thunar": "File Manager",
                        "gedit": "Text Editor",
                        "vim": "Vim",
                        "emacs": "Emacs",
                        "libreoffice": "LibreOffice",
                        "gimp": "GIMP",
                        "slack": "Slack",
                        "discord": "Discord",
                        "telegram": "Telegram",
                        "zoom": "Zoom",
                        "vlc": "VLC Media Player",
                        "spotify": "Spotify",
                        "thunderbird": "Thunderbird"
                    }
                    
                    # Check if Firefox is active and try to get URL
                    if app_name_clean == "firefox":
                        firefox_url = self.get_firefox_url_linux(window_id)
                        if firefox_url:
                            log_data = {
                                "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                                "active_app": firefox_url,  # URL instead of app name
                                "module": "Firefox"
                            }
                            return [log_data]
                    
                    # Replace with friendly name if available
                    if app_name_clean in friendly_names:
                        app_name = friendly_names[app_name_clean]
                    else:
                        app_name = app_name_clean.replace("-", " ").title()
                        
                except psutil.NoSuchProcess:
                    logger.info("Process with PID %s no longer exists", pid)
                    app_name = None
                except Exception as e:
                    logger.info("Error getting process info for PID %s: %s", pid, e)
                    app_name = None
            
            # Fallback to window title if we couldn't get process name
            if not app_name:
                window_name = None
                
                # Try UTF-8 _NET_WM_NAME
                prop = window.get_full_property(self.NET_WM_NAME, 0)
                if prop:
                    value = prop.value
                    if isinstance(value, bytes):
                        window_name = value.decode("utf-8", errors="ignore")
                    else:
                        window_name = str(value)

                # Fallback to legacy WM_NAME
                if not window_name:
                    prop = window.get_full_property(self.WM_NAME, 0)
                    if prop:
                        value = prop.value
                        if isinstance(value, bytes):
                            window_name = value.decode("latin1", errors="ignore")
                        else:
                            window_name = str(value)

                # Extract app name from window title (rough heuristic)
                if window_name:
                    # Try to extract app name from patterns like "filename - AppName"
                    if " - " in window_name:
                        app_name = window_name.split(" - ")[-1]
                    else:
                        app_name = window_name
                    
                    app_name = self.rename(app_name)

            if not app_name:
                app_name = "Unknown Application"

            # Try to get process path for icon extraction
            app_path = None
            try:
                # Get PID from window
                NET_WM_PID = self.d.intern_atom('_NET_WM_PID')
                pid_prop = window.get_full_property(NET_WM_PID, X.AnyPropertyType)
                if pid_prop and pid_prop.value:
                    pid = pid_prop.value[0]
                    try:
                        process = psutil.Process(pid)
                        app_path = process.exe()
                    except (psutil.NoSuchProcess, psutil.AccessDenied):
                        pass
            except Exception:
                pass  # Path lookup is optional

            log_data = {
                "timestamp": str(time.strftime('%Y-%m-%d %H:%M:%S')),
                "module": app_name,
                "active_app": app_name,
                "url": app_name,
                "app_path": app_path,  # Add executable path for icon extraction
                "app_display_name": app_name  # Add display name
            }

            return [log_data]

        except Exception as e:
            logger.info("Error extracting active app info: %s", e)
            return []

    def get_firefox_url_linux(self, window_id):
        """
        Extract the active URL from Firefox on Linux using xdotool and clipboard
        """
        try:
            # Method 2: Try to read from Firefox's database
            try:
                firefox_url = self.get_firefox_url_from_database()
                if firefox_url:
                    return firefox_url
            except Exception as e:
                logger.info("Failed to get Firefox URL from database: %s", e)
            
            return None
            
        except Exception as e:
            logger.error("Error getting Firefox URL on Linux: %s", e)
            return None
    
    def get_app_icon(self, app_name: str, app_path: Optional[str] = None) -> Optional[str]:
        """
        Extract app icon and return as base64 string or file path.
        
        Args:
            app_name: Application name
            app_path: Path to application executable/bundle (optional, will try to find if not provided)
            
        Returns:
            base64 encoded icon string, or None if extraction fails
        """
        try:
            import base64
            from io import BytesIO
            
            os_type = get_os_type()
            
            if os_type == "Windows":
                return self._get_app_icon_windows(app_name, app_path)
            elif os_type == "Darwin":
                return self._get_app_icon_mac(app_name, app_path)
            elif os_type == "Linux":
                return self._get_app_icon_linux(app_name, app_path)
            else:
                logger.warning("Unsupported OS type for icon extraction: %s", os_type)
                return None
                
        except Exception as e:
            logger.error("Error extracting app icon: %s", e, exc_info=True)
            return None
    
    def _get_app_icon_windows(self, app_name: str, app_path: Optional[str] = None) -> Optional[str]:
        """Extract icon from Windows executable."""
        try:
            import win32gui
            import win32ui
            import win32con
            from PIL import Image
            import base64
            from io import BytesIO
            
            if not app_path:
                # Try to find executable path
                try:
                    import psutil
                    for proc in psutil.process_iter(['pid', 'name', 'exe']):
                        try:
                            if proc.info['name'] and app_name.lower() in proc.info['name'].lower():
                                app_path = proc.info.get('exe')
                                if app_path and os.path.exists(app_path):
                                    break
                        except (psutil.NoSuchProcess, psutil.AccessDenied):
                            continue
                except Exception:
                    pass
            
            if not app_path or not os.path.exists(app_path):
                logger.debug("Could not find executable path for %s", app_name)
                return None
            
            # Extract icon from executable
            try:
                large, small = win32gui.ExtractIconEx(app_path, 0)
                if large:
                    hicon = large[0]
                    # Get icon info
                    icon_info = win32gui.GetIconInfo(hicon)
                    hbm = icon_info[3]  # hbmColor
                    
                    # Convert to PIL Image
                    bmp = win32ui.CreateBitmapFromHandle(hbm)
                    bmp_info = bmp.GetInfo()
                    bmp_str = bmp.GetBitmapBits(True)
                    
                    img = Image.frombuffer(
                        'RGBA',
                        (bmp_info['bmWidth'], bmp_info['bmHeight']),
                        bmp_str, 'raw', 'BGRA', 0, 1
                    )
                    
                    # Resize to 24x24
                    img = img.resize((24, 24), Image.Resampling.LANCZOS)
                    
                    # Convert to base64
                    buffer = BytesIO()
                    img.save(buffer, format='PNG')
                    img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
                    
                    # Cleanup
                    win32gui.DestroyIcon(hicon)
                    win32gui.DestroyIcon(small[0] if small else None)
                    
                    return f"data:image/png;base64,{img_base64}"
            except Exception as e:
                logger.debug("Failed to extract icon from %s: %s", app_path, e)
                return None
                
        except ImportError:
            logger.debug("win32gui not available for icon extraction")
            return None
        except Exception as e:
            logger.debug("Error extracting Windows icon: %s", e)
            return None
    
    def _get_app_icon_mac(self, app_name: str, app_path: Optional[str] = None) -> Optional[str]:
        """Extract icon from macOS app bundle."""
        try:
            from PIL import Image
            import base64
            from io import BytesIO
            
            if not app_path:
                # Try to find app bundle
                app_bundle_name = f"{app_name}.app"
                search_paths = [
                    f"/Applications/{app_bundle_name}",
                    f"{os.path.expanduser('~/Applications')}/{app_bundle_name}",
                ]
                
                for path in search_paths:
                    if os.path.exists(path):
                        app_path = path
                        break
            
            if not app_path or not os.path.exists(app_path):
                # Try using NSWorkspace to get icon
                if HAS_APPKIT:
                    try:
                        workspace = NSWorkspace.sharedWorkspace()
                        # Try to find app by bundle identifier or name
                        app_url = workspace.findApplication(app_name)
                        if app_url:
                            icon = workspace.iconForFile(str(app_url))
                            if icon:
                                # Convert NSImage to PIL Image
                                from AppKit import NSBitmapImageRep, NSImage
                                img_data = icon.TIFFRepresentation()
                                img = Image.open(BytesIO(img_data))
                                img = img.resize((24, 24), Image.Resampling.LANCZOS)
                                
                                buffer = BytesIO()
                                img.save(buffer, format='PNG')
                                img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
                                return f"data:image/png;base64,{img_base64}"
                    except Exception as e:
                        logger.debug("Failed to get icon via NSWorkspace: %s", e)
                
                logger.debug("Could not find app bundle for %s", app_name)
                return None
            
            # Extract icon from .app bundle
            icon_path = os.path.join(app_path, "Contents", "Resources", f"{app_name}.icns")
            if not os.path.exists(icon_path):
                # Try default icon name
                icon_path = os.path.join(app_path, "Contents", "Resources", "AppIcon.icns")
            
            if os.path.exists(icon_path):
                # Use sips or convert icns to png
                try:
                    # Try using sips (macOS built-in)
                    import subprocess
                    result = subprocess.run(
                        ['sips', '-s', 'format', 'png', icon_path, '--out', '/tmp/app_icon_temp.png'],
                        capture_output=True, timeout=2
                    )
                    if result.returncode == 0 and os.path.exists('/tmp/app_icon_temp.png'):
                        img = Image.open('/tmp/app_icon_temp.png')
                        img = img.resize((24, 24), Image.Resampling.LANCZOS)
                        
                        buffer = BytesIO()
                        img.save(buffer, format='PNG')
                        img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
                        
                        # Cleanup temp file
                        try:
                            os.remove('/tmp/app_icon_temp.png')
                        except Exception:
                            pass
                        
                        return f"data:image/png;base64,{img_base64}"
                except Exception as e:
                    logger.debug("Failed to convert icns using sips: %s", e)
            
            return None
            
        except Exception as e:
            logger.debug("Error extracting macOS icon: %s", e)
            return None
    
    def _get_app_icon_linux(self, app_name: str, app_path: Optional[str] = None) -> Optional[str]:
        """Extract icon from Linux desktop file or system theme."""
        try:
            from PIL import Image
            import base64
            from io import BytesIO
            
            # Try to find .desktop file
            desktop_dirs = [
                os.path.expanduser("~/.local/share/applications"),
                "/usr/share/applications",
                "/usr/local/share/applications",
            ]
            
            icon_name = None
            for desktop_dir in desktop_dirs:
                if not os.path.exists(desktop_dir):
                    continue
                    
                for filename in os.listdir(desktop_dir):
                    if filename.endswith('.desktop'):
                        desktop_path = os.path.join(desktop_dir, filename)
                        try:
                            with open(desktop_path, 'r', encoding='utf-8') as f:
                                content = f.read()
                                # Check if this desktop file matches the app
                                if app_name.lower() in content.lower() or app_name.lower() in filename.lower():
                                    # Extract Icon= line
                                    for line in content.split('\n'):
                                        if line.startswith('Icon='):
                                            icon_name = line.split('=', 1)[1].strip()
                                            break
                                    if icon_name:
                                        break
                        except Exception:
                            continue
                    if icon_name:
                        break
                if icon_name:
                    break
            
            if not icon_name:
                # Use app name as fallback
                icon_name = app_name.lower()
            
            # Try to find icon in theme directories
            icon_dirs = [
                os.path.expanduser("~/.local/share/icons"),
                "/usr/share/icons",
                "/usr/share/pixmaps",
            ]
            
            icon_extensions = ['.png', '.svg', '.xpm']
            for icon_dir in icon_dirs:
                if not os.path.exists(icon_dir):
                    continue
                    
                # Search in theme subdirectories
                for root, dirs, files in os.walk(icon_dir):
                    for file in files:
                        if file.startswith(icon_name) and any(file.endswith(ext) for ext in icon_extensions):
                            icon_path = os.path.join(root, file)
                            try:
                                if file.endswith('.svg'):
                                    # SVG handling - convert to PNG
                                    try:
                                        import cairosvg
                                        png_data = cairosvg.svg2png(url=icon_path, output_width=24, output_height=24)
                                        img_base64 = base64.b64encode(png_data).decode('utf-8')
                                        return f"data:image/png;base64,{img_base64}"
                                    except ImportError:
                                        logger.debug("cairosvg not available for SVG conversion")
                                        continue
                                else:
                                    img = Image.open(icon_path)
                                    img = img.resize((24, 24), Image.Resampling.LANCZOS)
                                    
                                    buffer = BytesIO()
                                    img.save(buffer, format='PNG')
                                    img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
                                    return f"data:image/png;base64,{img_base64}"
                            except Exception as e:
                                logger.debug("Failed to load icon %s: %s", icon_path, e)
                                continue
            
            return None
            
        except Exception as e:
            logger.debug("Error extracting Linux icon: %s", e)
            return None
        