"""Downloads watchdog for monitoring and handling file downloads."""

import asyncio
import json
import os
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar
from urllib.parse import urlparse

import anyio
from bubus import BaseEvent
from cdp_use.cdp.browser import DownloadProgressEvent as CDPDownloadProgressEvent
from cdp_use.cdp.browser import DownloadWillBeginEvent
from cdp_use.cdp.network import ResponseReceivedEvent
from cdp_use.cdp.target import SessionID, TargetID
from pydantic import PrivateAttr

from noesium.subagents.bu.browser.events import (
    BrowserLaunchEvent,
    BrowserStateRequestEvent,
    BrowserStoppedEvent,
    DownloadProgressEvent,
    DownloadStartedEvent,
    FileDownloadedEvent,
    NavigationCompleteEvent,
    TabClosedEvent,
    TabCreatedEvent,
)
from noesium.subagents.bu.browser.watchdog_base import BaseWatchdog
from noesium.subagents.bu.utils import create_task_with_error_handling

if TYPE_CHECKING:
    pass


class DownloadsWatchdog(BaseWatchdog):
    """Monitors downloads and handles file download events."""

    # Events this watchdog listens to (for documentation)
    LISTENS_TO: ClassVar[list[type[BaseEvent[Any]]]] = [
        BrowserLaunchEvent,
        BrowserStateRequestEvent,
        BrowserStoppedEvent,
        TabCreatedEvent,
        TabClosedEvent,
        NavigationCompleteEvent,
    ]

    # Events this watchdog emits
    EMITS: ClassVar[list[type[BaseEvent[Any]]]] = [
        DownloadProgressEvent,
        DownloadStartedEvent,
        FileDownloadedEvent,
    ]

    # Private state
    _sessions_with_listeners: set[str] = PrivateAttr(
        default_factory=set
    )  # Track sessions that already have download listeners
    _active_downloads: dict[str, Any] = PrivateAttr(default_factory=dict)
    _pdf_viewer_cache: dict[str, bool] = PrivateAttr(default_factory=dict)  # Cache PDF viewer status by target URL
    _download_cdp_session_setup: bool = PrivateAttr(default=False)  # Track if CDP session is set up
    _download_cdp_session: Any = PrivateAttr(default=None)  # Store CDP session reference
    _cdp_event_tasks: set[asyncio.Task] = PrivateAttr(default_factory=set)  # Track CDP event handler tasks
    _cdp_downloads_info: dict[str, dict[str, Any]] = PrivateAttr(default_factory=dict)  # Map guid -> info
    _session_pdf_urls: dict[str, str] = PrivateAttr(
        default_factory=dict
    )  # URL -> path for PDFs downloaded this session
    _initial_downloads_snapshot: set[str] = PrivateAttr(default_factory=set)  # Files present when watchdog started
    _network_monitored_targets: set[str] = PrivateAttr(
        default_factory=set
    )  # Track targets with network monitoring enabled
    _detected_downloads: set[str] = PrivateAttr(default_factory=set)  # Track detected download URLs to avoid duplicates
    _network_callback_registered: bool = PrivateAttr(default=False)  # Track if global network callback is registered

    # Direct callback support for download waiting (bypasses event bus for synchronization)
    _download_start_callbacks: list[Any] = PrivateAttr(default_factory=list)  # Callbacks for download start
    _download_progress_callbacks: list[Any] = PrivateAttr(default_factory=list)  # Callbacks for download progress
    _download_complete_callbacks: list[Any] = PrivateAttr(default_factory=list)  # Callbacks for download complete

    def register_download_callbacks(
        self,
        on_start: Any | None = None,
        on_progress: Any | None = None,
        on_complete: Any | None = None,
    ) -> None:
        """Register direct callbacks for download events

        Callbacks called sync from CDP event handlers, so click
        handlers receive download notif without waiting for event bus to process
        """
        self.logger.debug(
            f"[DownloadsWatchdog] Registering callbacks: start={on_start is not None}, progress={on_progress is not None}, complete={on_complete is not None}"
        )
        if on_start:
            self._download_start_callbacks.append(on_start)
            self.logger.debug(
                f"[DownloadsWatchdog] Registered start callback, now have {len(self._download_start_callbacks)} start callbacks"
            )
        if on_progress:
            self._download_progress_callbacks.append(on_progress)
        if on_complete:
            self._download_complete_callbacks.append(on_complete)

    def unregister_download_callbacks(
        self,
        on_start: Any | None = None,
        on_progress: Any | None = None,
        on_complete: Any | None = None,
    ) -> None:
        """Unregister previously registered download callbacks."""
        if on_start and on_start in self._download_start_callbacks:
            self._download_start_callbacks.remove(on_start)
        if on_progress and on_progress in self._download_progress_callbacks:
            self._download_progress_callbacks.remove(on_progress)
        if on_complete and on_complete in self._download_complete_callbacks:
            self._download_complete_callbacks.remove(on_complete)

    async def on_BrowserLaunchEvent(self, event: BrowserLaunchEvent) -> None:
        self.logger.debug(f"[DownloadsWatchdog] Received BrowserLaunchEvent, EventBus ID: {id(self.event_bus)}")
        # Ensure downloads directory exists
        downloads_path = self.browser_session.browser_profile.downloads_path
        if downloads_path:
            expanded_path = Path(downloads_path).expanduser().resolve()
            expanded_path.mkdir(parents=True, exist_ok=True)
            self.logger.debug(f"[DownloadsWatchdog] Ensured downloads directory exists: {expanded_path}")

            # Capture initial files to detect new downloads reliably
            if expanded_path.exists():
                for f in expanded_path.iterdir():
                    if f.is_file() and not f.name.startswith("."):
                        self._initial_downloads_snapshot.add(f.name)
                self.logger.debug(
                    f"[DownloadsWatchdog] Captured initial downloads: {len(self._initial_downloads_snapshot)} files"
                )

    async def on_TabCreatedEvent(self, event: TabCreatedEvent) -> None:
        """Monitor new tabs for downloads."""
        # logger.info(f'[DownloadsWatchdog] TabCreatedEvent received for tab {event.target_id[-4:]}: {event.url}')

        # Assert downloads path is configured (should always be set by BrowserProfile default)
        assert self.browser_session.browser_profile.downloads_path is not None, "Downloads path must be configured"

        if event.target_id:
            # logger.info(f'[DownloadsWatchdog] Found target for tab {event.target_id}, calling attach_to_target')
            await self.attach_to_target(event.target_id)
        else:
            self.logger.warning(f"[DownloadsWatchdog] No target found for tab {event.target_id}")

    async def on_TabClosedEvent(self, event: TabClosedEvent) -> None:
        """Stop monitoring closed tabs."""
        pass  # No cleanup needed, browser context handles target lifecycle

    async def on_BrowserStateRequestEvent(self, event: BrowserStateRequestEvent) -> None:
        """Handle browser state request events."""
        # Use public API - automatically validates and waits for recovery if needed
        self.logger.debug(f"[DownloadsWatchdog] on_BrowserStateRequestEvent started, event_id={event.event_id[-4:]}")
        try:
            cdp_session = await self.browser_session.get_or_create_cdp_session()
        except ValueError:
            self.logger.warning(
                f"[DownloadsWatchdog] No valid focus, skipping BrowserStateRequestEvent {event.event_id[-4:]}"
            )
            return  # No valid focus, skip

        self.logger.debug(
            f'[DownloadsWatchdog] About to call get_current_page_url(), target_id={cdp_session.target_id[-4:] if cdp_session.target_id else "None"}'
        )
        url = await self.browser_session.get_current_page_url()
        self.logger.debug(f'[DownloadsWatchdog] Got URL: {url[:80] if url else "None"}')

        if not url:
            self.logger.warning(f"[DownloadsWatchdog] No URL found for BrowserStateRequestEvent {event.event_id[-4:]}")
            return

        target_id = cdp_session.target_id
        self.logger.debug(f"[DownloadsWatchdog] About to dispatch NavigationCompleteEvent for target {target_id[-4:]}")
        self.event_bus.dispatch(
            NavigationCompleteEvent(
                event_type="NavigationCompleteEvent",
                url=url,
                target_id=target_id,
                event_parent_id=event.event_id,
            )
        )
        self.logger.debug("[DownloadsWatchdog] Successfully completed BrowserStateRequestEvent")

    async def on_BrowserStoppedEvent(self, event: BrowserStoppedEvent) -> None:
        """Clean up when browser stops."""
        # Cancel all CDP event handler tasks
        for task in list(self._cdp_event_tasks):
            if not task.done():
                task.cancel()
        # Wait for all tasks to complete cancellation
        if self._cdp_event_tasks:
            await asyncio.gather(*self._cdp_event_tasks, return_exceptions=True)
        self._cdp_event_tasks.clear()

        # Clean up CDP session
        # CDP sessions are now cached and managed by BrowserSession
        self._download_cdp_session = None
        self._download_cdp_session_setup = False

        # Clear other state
        self._sessions_with_listeners.clear()
        self._active_downloads.clear()
        self._pdf_viewer_cache.clear()
        self._session_pdf_urls.clear()
        self._network_monitored_targets.clear()
        self._detected_downloads.clear()
        self._initial_downloads_snapshot.clear()
        self._network_callback_registered = False

    async def on_NavigationCompleteEvent(self, event: NavigationCompleteEvent) -> None:
        """Check for PDFs after navigation completes."""
        self.logger.debug(
            f"[DownloadsWatchdog] NavigationCompleteEvent received for {event.url}, tab #{event.target_id[-4:]}"
        )

        # Clear PDF cache for the navigated URL since content may have changed
        if event.url in self._pdf_viewer_cache:
            del self._pdf_viewer_cache[event.url]

        # Check if auto-download is enabled
        auto_download_enabled = self._is_auto_download_enabled()
        if not auto_download_enabled:
            return

        # Note: Using network-based PDF detection that doesn't require JavaScript

        target_id = event.target_id
        self.logger.debug(f"[DownloadsWatchdog] Got target_id={target_id} for tab #{event.target_id[-4:]}")

        is_pdf = await self.check_for_pdf_viewer(target_id)

        if is_pdf:
            self.logger.debug(f"[DownloadsWatchdog] 📄 PDF detected at {event.url}, triggering auto-download...")
            download_path = await self.trigger_pdf_download(target_id)
            if not download_path:
                self.logger.warning(f"[DownloadsWatchdog] ⚠️ PDF download failed for {event.url}")

    def _is_auto_download_enabled(self) -> bool:
        """Check if auto-download PDFs is enabled in browser profile."""
        return self.browser_session.browser_profile.auto_download_pdfs

    async def attach_to_target(self, target_id: TargetID) -> None:
        """Set up download monitoring for a specific target."""

        # Define CDP event handlers outside of try to avoid indentation/scope issues
        def download_will_begin_handler(event: DownloadWillBeginEvent, session_id: SessionID | None) -> None:
            self.logger.debug(f"[DownloadsWatchdog] Download will begin: {event}")
            # Cache info for later completion event handling (esp. remote browsers)
            guid = event.get("guid", "")
            url = event.get("url", "")
            suggested_filename = event.get("suggestedFilename", "download")
            try:
                assert suggested_filename, "CDP DownloadWillBegin missing suggestedFilename"
                self._cdp_downloads_info[guid] = {
                    "url": url,
                    "suggested_filename": suggested_filename,
                    "handled": False,
                }
            except (AssertionError, KeyError):
                pass

            # Call direct callbacks first (for click handlers waiting for downloads)
            download_info = {
                "guid": guid,
                "url": url,
                "suggested_filename": suggested_filename,
                "auto_download": False,
            }
            self.logger.debug(f"[DownloadsWatchdog] Calling {len(self._download_start_callbacks)} start callbacks")
            for callback in self._download_start_callbacks:
                try:
                    self.logger.debug(f"[DownloadsWatchdog] Calling start callback: {callback}")
                    callback(download_info)
                except Exception as e:
                    self.logger.debug(f"[DownloadsWatchdog] Error in download start callback: {e}")

            # Emit DownloadStartedEvent so other components can react
            self.event_bus.dispatch(
                DownloadStartedEvent(
                    guid=guid,
                    url=url,
                    suggested_filename=suggested_filename,
                    auto_download=False,  # CDP-triggered downloads are user-initiated
                )
            )

            # Create and track the task
            task = create_task_with_error_handling(
                self._handle_cdp_download(event, target_id, session_id),
                name="handle_cdp_download",
                logger_instance=self.logger,
                suppress_exceptions=True,
            )
            self._cdp_event_tasks.add(task)
            # Remove from set when done
            task.add_done_callback(lambda t: self._cdp_event_tasks.discard(t))

        def download_progress_handler(event: CDPDownloadProgressEvent, session_id: SessionID | None) -> None:
            guid = event.get("guid", "")
            state = event.get("state", "")
            received_bytes = int(event.get("receivedBytes", 0))
            total_bytes = int(event.get("totalBytes", 0))

            # Call direct callbacks first (for click handlers tracking progress)
            progress_info = {
                "guid": guid,
                "received_bytes": received_bytes,
                "total_bytes": total_bytes,
                "state": state,
            }
            for callback in self._download_progress_callbacks:
                try:
                    callback(progress_info)
                except Exception as e:
                    self.logger.debug(f"[DownloadsWatchdog] Error in download progress callback: {e}")

            # Emit progress event for all states so listeners can track progress
            from noesium.subagents.bu.browser.events import DownloadProgressEvent as DownloadProgressEventInternal

            self.event_bus.dispatch(
                DownloadProgressEventInternal(
                    guid=guid,
                    received_bytes=received_bytes,
                    total_bytes=total_bytes,
                    state=state,
                )
            )

            # Check if download is complete
            if state == "completed":
                file_path = event.get("filePath")
                if self.browser_session.is_local:
                    if file_path:
                        self.logger.debug(f"[DownloadsWatchdog] Download completed: {file_path}")
                        # Track the download
                        self._track_download(file_path, guid=guid)
                        # Mark as handled to prevent fallback duplicate dispatch
                        try:
                            if guid in self._cdp_downloads_info:
                                self._cdp_downloads_info[guid]["handled"] = True
                        except (KeyError, AttributeError):
                            pass
                    else:
                        # No filePath provided - detect by comparing with initial snapshot
                        self.logger.debug("[DownloadsWatchdog] No filePath in progress event; detecting via filesystem")
                        downloads_path = self.browser_session.browser_profile.downloads_path
                        if downloads_path:
                            downloads_dir = Path(downloads_path).expanduser().resolve()
                            if downloads_dir.exists():
                                for f in downloads_dir.iterdir():
                                    if (
                                        f.is_file()
                                        and not f.name.startswith(".")
                                        and f.name not in self._initial_downloads_snapshot
                                    ):
                                        # Check file has content before processing
                                        if f.stat().st_size > 4:
                                            # Found a new file! Add to snapshot immediately to prevent duplicate detection
                                            self._initial_downloads_snapshot.add(f.name)
                                            self.logger.debug(f"[DownloadsWatchdog] Detected new download: {f.name}")
                                            self._track_download(str(f))
                                            # Mark as handled
                                            try:
                                                if guid in self._cdp_downloads_info:
                                                    self._cdp_downloads_info[guid]["handled"] = True
                                            except (KeyError, AttributeError):
                                                pass
                                            break
                else:
                    # Remote browser: do not touch local filesystem. Fallback to downloadPath+suggestedFilename
                    info = self._cdp_downloads_info.get(guid, {})
                    try:
                        suggested_filename = info.get("suggested_filename") or (
                            Path(file_path).name if file_path else "download"
                        )
                        downloads_path = str(self.browser_session.browser_profile.downloads_path or "")
                        effective_path = file_path or str(Path(downloads_path) / suggested_filename)
                        file_name = Path(effective_path).name
                        file_ext = Path(file_name).suffix.lower().lstrip(".")
                        self.event_bus.dispatch(
                            FileDownloadedEvent(
                                guid=guid,
                                url=info.get("url", ""),
                                path=str(effective_path),
                                file_name=file_name,
                                file_size=0,
                                file_type=file_ext if file_ext else None,
                            )
                        )
                        self.logger.debug(f"[DownloadsWatchdog] ✅ (remote) Download completed: {effective_path}")
                    finally:
                        if guid in self._cdp_downloads_info:
                            del self._cdp_downloads_info[guid]

        try:
            downloads_path_raw = self.browser_session.browser_profile.downloads_path
            if not downloads_path_raw:
                # logger.info(f'[DownloadsWatchdog] No downloads path configured, skipping target: {target_id}')
                return  # No downloads path configured

            # Check if we already have a download listener on this session
            # to prevent duplicate listeners from being added
            # Note: Since download listeners are set up once per browser session, not per target,
            # we just track if we've set up the browser-level listener
            if self._download_cdp_session_setup:
                self.logger.debug("[DownloadsWatchdog] Download listener already set up for browser session")
                return

            # logger.debug(f'[DownloadsWatchdog] Setting up CDP download listener for target: {target_id}')

            # Use CDP session for download events but store reference in watchdog
            if not self._download_cdp_session_setup:
                # Set up CDP session for downloads (only once per browser session)
                cdp_client = self.browser_session.cdp_client

                # Set download behavior to allow downloads and enable events
                downloads_path = self.browser_session.browser_profile.downloads_path
                if not downloads_path:
                    self.logger.warning("[DownloadsWatchdog] No downloads path configured, skipping CDP download setup")
                    return
                # Ensure path is properly expanded (~ -> absolute path)
                expanded_downloads_path = Path(downloads_path).expanduser().resolve()
                await cdp_client.send.Browser.setDownloadBehavior(
                    params={
                        "behavior": "allow",
                        "downloadPath": str(expanded_downloads_path),  # Use expanded absolute path
                        "eventsEnabled": True,
                    }
                )

                # Register the handlers with CDP
                cdp_client.register.Browser.downloadWillBegin(download_will_begin_handler)  # type: ignore[arg-type]
                cdp_client.register.Browser.downloadProgress(download_progress_handler)  # type: ignore[arg-type]

                self._download_cdp_session_setup = True
                self.logger.debug("[DownloadsWatchdog] Set up CDP download listeners")

            # No need to track individual targets since download listener is browser-level
            # logger.debug(f'[DownloadsWatchdog] Successfully set up CDP download listener for target: {target_id}')

        except Exception as e:
            self.logger.warning(
                f"[DownloadsWatchdog] Failed to set up CDP download listener for target {target_id}: {e}"
            )

        # Set up network monitoring for this target (catches ALL download variants)
        await self._setup_network_monitoring(target_id)

    async def _setup_network_monitoring(self, target_id: TargetID) -> None:
        """Set up network monitoring to detect PDFs and downloads from ALL sources.

        This catches:
        - Direct PDF navigation
        - PDFs in iframes
        - PDFs with embed/object tags
        - JavaScript-triggered downloads
        - Any Content-Disposition: attachment headers
        """
        # Skip if already monitoring this target
        if target_id in self._network_monitored_targets:
            self.logger.debug(f"[DownloadsWatchdog] Network monitoring already enabled for target {target_id[-4:]}")
            return

        # Check if auto-download is enabled
        if not self._is_auto_download_enabled():
            self.logger.debug("[DownloadsWatchdog] Auto-download disabled, skipping network monitoring")
            return

        try:
            cdp_client = self.browser_session.cdp_client

            # Register the global callback once
            if not self._network_callback_registered:

                def on_response_received(event: ResponseReceivedEvent, session_id: str | None) -> None:
                    """Handle Network.responseReceived event to detect downloadable content.

                    This callback is registered globally and uses session_id to determine the correct target.
                    """
                    try:
                        # Check if session_manager exists (may be None during browser shutdown)
                        if not self.browser_session.session_manager:
                            self.logger.warning(
                                "[DownloadsWatchdog] Session manager not found, skipping network monitoring"
                            )
                            return

                        # Look up target_id from session_id
                        event_target_id = self.browser_session.session_manager.get_target_id_from_session_id(session_id)
                        if not event_target_id:
                            # Session not in pool - might be a stale session or not yet tracked
                            return

                        # Only process events for targets we're monitoring
                        if event_target_id not in self._network_monitored_targets:
                            return

                        response = event.get("response", {})
                        url = response.get("url", "")
                        content_type = response.get("mimeType", "").lower()
                        headers = {
                            k.lower(): v for k, v in response.get("headers", {}).items()
                        }  # Normalize for case-insensitive lookup
                        request_type = event.get("type", "")

                        # Skip non-HTTP URLs (data:, about:, chrome-extension:, etc.)
                        if not url.startswith("http"):
                            return

                        # Skip fetch/XHR - real browsers don't download PDFs from programmatic requests
                        if request_type in ("Fetch", "XHR"):
                            return

                        # Check if it's a PDF
                        is_pdf = "application/pdf" in content_type

                        # Check if it's marked as download via Content-Disposition header
                        content_disposition = str(headers.get("content-disposition", "")).lower()
                        is_download_attachment = "attachment" in content_disposition

                        # Filter out image/video/audio files even if marked as attachment
                        # These are likely resources, not intentional downloads
                        unwanted_content_types = [
                            "image/",
                            "video/",
                            "audio/",
                            "text/css",
                            "text/javascript",
                            "application/javascript",
                            "application/x-javascript",
                            "text/html",
                            "application/json",
                            "font/",
                            "application/font",
                            "application/x-font",
                        ]
                        is_unwanted_type = any(content_type.startswith(prefix) for prefix in unwanted_content_types)
                        if is_unwanted_type:
                            return

                        # Check URL extension to filter out obvious images/resources
                        url_lower = url.lower().split("?")[0]  # Remove query params
                        unwanted_extensions = [
                            ".jpg",
                            ".jpeg",
                            ".png",
                            ".gif",
                            ".webp",
                            ".svg",
                            ".ico",
                            ".css",
                            ".js",
                            ".woff",
                            ".woff2",
                            ".ttf",
                            ".eot",
                            ".mp4",
                            ".webm",
                            ".mp3",
                            ".wav",
                            ".ogg",
                        ]
                        if any(url_lower.endswith(ext) for ext in unwanted_extensions):
                            return

                        # Only process if it's a PDF or download
                        if not (is_pdf or is_download_attachment):
                            return

                        # If already downloaded this URL and file still exists, do nothing
                        existing_path = self._session_pdf_urls.get(url)
                        if existing_path:
                            if os.path.exists(existing_path):
                                return
                            # Stale cache entry, allow re-download
                            del self._session_pdf_urls[url]

                        # Check if we've already processed this URL in this session
                        if url in self._detected_downloads:
                            self.logger.debug(f"[DownloadsWatchdog] Already detected download: {url[:80]}...")
                            return

                        # Mark as detected to avoid duplicates
                        self._detected_downloads.add(url)

                        # Extract filename from Content-Disposition if available
                        suggested_filename = None
                        if "filename=" in content_disposition:
                            # Parse filename from Content-Disposition header
                            import re

                            filename_match = re.search(r'filename[^;=\n]*=(([\'"]).*?\2|[^;\n]*)', content_disposition)
                            if filename_match:
                                suggested_filename = filename_match.group(1).strip("'\"")

                        self.logger.info(
                            f"[DownloadsWatchdog] 🔍 Detected downloadable content via network: {url[:80]}..."
                        )
                        self.logger.debug(
                            f"[DownloadsWatchdog]   Content-Type: {content_type}, Is PDF: {is_pdf}, Is Attachment: {is_download_attachment}"
                        )

                        # Trigger download asynchronously in background (don't block event handler)
                        async def download_in_background():
                            # Don't permanently block re-processing this URL if download fails
                            try:
                                download_path = await self.download_file_from_url(
                                    url=url,
                                    target_id=event_target_id,  # Use target_id from session_id lookup
                                    content_type=content_type,
                                    suggested_filename=suggested_filename,
                                )

                                if download_path:
                                    self.logger.info(f"[DownloadsWatchdog] ✅ Successfully downloaded: {download_path}")
                                else:
                                    self.logger.warning(f"[DownloadsWatchdog] ⚠️  Failed to download: {url[:80]}...")
                            except Exception as e:
                                self.logger.error(
                                    f"[DownloadsWatchdog] Error downloading in background: {type(e).__name__}: {e}"
                                )
                            finally:
                                # Allow future detections of the same URL
                                self._detected_downloads.discard(url)

                        # Create background task
                        task = create_task_with_error_handling(
                            download_in_background(),
                            name="download_in_background",
                            logger_instance=self.logger,
                            suppress_exceptions=True,
                        )
                        self._cdp_event_tasks.add(task)
                        task.add_done_callback(lambda t: self._cdp_event_tasks.discard(t))

                    except Exception as e:
                        self.logger.error(
                            f"[DownloadsWatchdog] Error in network response handler: {type(e).__name__}: {e}"
                        )

                # Register the callback globally (once)
                cdp_client.register.Network.responseReceived(on_response_received)
                self._network_callback_registered = True
                self.logger.debug("[DownloadsWatchdog] ✅ Registered global network response callback")

            # Get or create CDP session for this target
            cdp_session = await self.browser_session.get_or_create_cdp_session(target_id, focus=False)

            # Enable Network domain to monitor HTTP responses (per-target/per-session)
            await cdp_client.send.Network.enable(session_id=cdp_session.session_id)
            self.logger.debug(f"[DownloadsWatchdog] Enabled Network domain for target {target_id[-4:]}")

            # Mark this target as monitored
            self._network_monitored_targets.add(target_id)
            self.logger.debug(f"[DownloadsWatchdog] ✅ Network monitoring enabled for target {target_id[-4:]}")

        except Exception as e:
            self.logger.warning(f"[DownloadsWatchdog] Failed to set up network monitoring for target {target_id}: {e}")

    async def download_file_from_url(
        self, url: str, target_id: TargetID, content_type: str | None = None, suggested_filename: str | None = None
    ) -> str | None:
        """Generic method to download any file from a URL.

        Args:
                url: The URL to download
                target_id: The target ID for CDP session
                content_type: Optional content type (e.g., 'application/pdf')
                suggested_filename: Optional filename from Content-Disposition header

        Returns:
                Path to downloaded file, or None if download failed
        """
        if not self.browser_session.browser_profile.downloads_path:
            self.logger.warning("[DownloadsWatchdog] No downloads path configured")
            return None

        # Check if already downloaded in this session
        if url in self._session_pdf_urls:
            existing_path = self._session_pdf_urls[url]
            if os.path.exists(existing_path):
                self.logger.debug(f"[DownloadsWatchdog] File already downloaded in session: {existing_path}")
                return existing_path

            # Stale cache entry: the file was removed/cleaned up after we cached it.
            self.logger.debug(
                f"[DownloadsWatchdog] Cached download path no longer exists, re-downloading: {existing_path}"
            )
            del self._session_pdf_urls[url]

        try:
            # Get or create CDP session for this target
            temp_session = await self.browser_session.get_or_create_cdp_session(target_id, focus=False)

            # Determine filename
            if suggested_filename:
                filename = suggested_filename
            else:
                # Extract from URL
                filename = os.path.basename(url.split("?")[0])  # Remove query params
                if not filename or "." not in filename:
                    # Fallback: use content type to determine extension
                    if content_type and "pdf" in content_type:
                        filename = "document.pdf"
                    else:
                        filename = "download"

            # Ensure downloads directory exists
            downloads_dir = str(self.browser_session.browser_profile.downloads_path)
            os.makedirs(downloads_dir, exist_ok=True)

            # Generate unique filename if file exists
            final_filename = filename
            existing_files = os.listdir(downloads_dir)
            if filename in existing_files:
                base, ext = os.path.splitext(filename)
                counter = 1
                while f"{base} ({counter}){ext}" in existing_files:
                    counter += 1
                final_filename = f"{base} ({counter}){ext}"
                self.logger.debug(f"[DownloadsWatchdog] File exists, using: {final_filename}")

            self.logger.debug(f"[DownloadsWatchdog] Downloading from: {url[:100]}...")

            # Download using JavaScript fetch to leverage browser cache
            escaped_url = json.dumps(url)

            result = await asyncio.wait_for(
                temp_session.cdp_client.send.Runtime.evaluate(
                    params={
                        "expression": f"""
				(async () => {{
					try {{
						const response = await fetch({escaped_url}, {{
							cache: 'force-cache'
						}});
						if (!response.ok) {{
							throw new Error(`HTTP error! status: ${{response.status}}`);
						}}
						const blob = await response.blob();
						const arrayBuffer = await blob.arrayBuffer();
						const uint8Array = new Uint8Array(arrayBuffer);

						return {{
							data: Array.from(uint8Array),
							responseSize: uint8Array.length
						}};
					}} catch (error) {{
						throw new Error(`Fetch failed: ${{error.message}}`);
					}}
				}})()
				""",
                        "awaitPromise": True,
                        "returnByValue": True,
                    },
                    session_id=temp_session.session_id,
                ),
                timeout=15.0,  # 15 second timeout
            )

            download_result = result.get("result", {}).get("value", {})

            if download_result and download_result.get("data") and len(download_result["data"]) > 0:
                download_path = os.path.join(downloads_dir, final_filename)

                # Save the file asynchronously
                async with await anyio.open_file(download_path, "wb") as f:
                    await f.write(bytes(download_result["data"]))

                # Verify file was written successfully
                if os.path.exists(download_path):
                    actual_size = os.path.getsize(download_path)
                    self.logger.debug(f"[DownloadsWatchdog] File written: {download_path} ({actual_size} bytes)")

                    # Determine file type
                    file_ext = Path(final_filename).suffix.lower().lstrip(".")
                    mime_type = content_type or f"application/{file_ext}"

                    # Store URL->path mapping for this session
                    self._session_pdf_urls[url] = download_path

                    # Emit file downloaded event
                    self.logger.debug(f"[DownloadsWatchdog] Dispatching FileDownloadedEvent for {final_filename}")
                    self.event_bus.dispatch(
                        FileDownloadedEvent(
                            url=url,
                            path=download_path,
                            file_name=final_filename,
                            file_size=actual_size,
                            file_type=file_ext if file_ext else None,
                            mime_type=mime_type,
                            auto_download=True,
                        )
                    )

                    return download_path
                else:
                    self.logger.error(f"[DownloadsWatchdog] Failed to write file: {download_path}")
                    return None
            else:
                self.logger.warning(f"[DownloadsWatchdog] No data received when downloading from {url}")
                return None

        except TimeoutError:
            self.logger.warning(f"[DownloadsWatchdog] Download timed out: {url[:80]}...")
            return None
        except Exception as e:
            self.logger.warning(f"[DownloadsWatchdog] Download failed: {type(e).__name__}: {e}")
            return None

    def _track_download(self, file_path: str, guid: str | None = None) -> None:
        """Track a completed download and dispatch the appropriate event.

        Args:
                file_path: The path to the downloaded file
                guid: Optional CDP download GUID for correlation with DownloadStartedEvent
        """
        try:
            # Get file info
            path = Path(file_path)
            if path.exists():
                file_size = path.stat().st_size
                self.logger.debug(f"[DownloadsWatchdog] Tracked download: {path.name} ({file_size} bytes)")

                # Get file extension for file_type
                file_ext = path.suffix.lower().lstrip(".")

                # Call direct callbacks first (for click handlers waiting for downloads)
                complete_info = {
                    "guid": guid,
                    "url": str(path),
                    "path": str(path),
                    "file_name": path.name,
                    "file_size": file_size,
                    "file_type": file_ext if file_ext else None,
                    "auto_download": False,
                }
                for callback in self._download_complete_callbacks:
                    try:
                        callback(complete_info)
                    except Exception as e:
                        self.logger.debug(f"[DownloadsWatchdog] Error in download complete callback: {e}")

                # Dispatch download event
                from noesium.subagents.bu.browser.events import FileDownloadedEvent

                self.event_bus.dispatch(
                    FileDownloadedEvent(
                        guid=guid,
                        url=str(path),  # Use the file path as URL for local files
                        path=str(path),
                        file_name=path.name,
                        file_size=file_size,
                    )
                )
            else:
                self.logger.warning(f"[DownloadsWatchdog] Downloaded file not found: {file_path}")
        except Exception as e:
            self.logger.error(f"[DownloadsWatchdog] Error tracking download: {e}")

    async def _handle_cdp_download(
        self, event: DownloadWillBeginEvent, target_id: TargetID, session_id: SessionID | None
    ) -> None:
        """Handle a CDP Page.downloadWillBegin event."""
        downloads_dir = (
            Path(
                self.browser_session.browser_profile.downloads_path
                or f"{tempfile.gettempdir()}/browser_use_downloads.{str(self.browser_session.id)[-4:]}"
            )
            .expanduser()
            .resolve()
        )  # Ensure path is properly expanded

        # Initialize variables that may be used outside try blocks
        file_size = 0
        download_url = event.get("url", "")
        suggested_filename = event.get("suggestedFilename", "download")
        guid = event.get("guid", "")

        try:
            self.logger.debug(
                f"[DownloadsWatchdog] ⬇️ File download starting: {suggested_filename} from {download_url[:100]}..."
            )
            self.logger.debug(f"[DownloadsWatchdog] Full CDP event: {event}")

            # Since Browser.setDownloadBehavior is already configured, the browser will download the file
            # We just need to wait for it to appear in the downloads directory
            downloads_dir / suggested_filename

            # For remote browsers, don't poll local filesystem; downloadProgress handler will emit the event
            if not self.browser_session.is_local:
                return
        except Exception as e:
            self.logger.error(f"[DownloadsWatchdog] ❌ Error handling CDP download: {type(e).__name__} {e}")

        # If we reach here, the fetch method failed, so wait for native download
        # Poll the downloads directory for new files
        self.logger.debug(
            f"[DownloadsWatchdog] Checking if browser auto-download saved the file for us: {suggested_filename}"
        )

        # Poll for new files
        max_wait = 20  # seconds
        start_time = asyncio.get_event_loop().time()

        while asyncio.get_event_loop().time() - start_time < max_wait:  # noqa: ASYNC110
            await asyncio.sleep(5.0)  # Check every 5 seconds

            if Path(downloads_dir).exists():
                for file_path in Path(downloads_dir).iterdir():
                    # Skip hidden files and files that were already there
                    if (
                        file_path.is_file()
                        and not file_path.name.startswith(".")
                        and file_path.name not in self._initial_downloads_snapshot
                    ):
                        # Add to snapshot immediately to prevent duplicate detection
                        self._initial_downloads_snapshot.add(file_path.name)
                        # Check if file has content (> 4 bytes)
                        try:
                            file_size = file_path.stat().st_size
                            if file_size > 4:
                                # Found a new download!
                                self.logger.debug(
                                    f"[DownloadsWatchdog] ✅ Found downloaded file: {file_path} ({file_size} bytes)"
                                )

                                # Determine file type from extension
                                file_ext = file_path.suffix.lower().lstrip(".")
                                file_type = file_ext if file_ext else None

                                # Dispatch download event
                                # Skip if already handled by progress/JS fetch
                                info = self._cdp_downloads_info.get(guid, {})
                                if info.get("handled"):
                                    return
                                self.event_bus.dispatch(
                                    FileDownloadedEvent(
                                        guid=guid,
                                        url=download_url,
                                        path=str(file_path),
                                        file_name=file_path.name,
                                        file_size=file_size,
                                        file_type=file_type,
                                    )
                                )
                            # Mark as handled after dispatch
                            try:
                                if guid in self._cdp_downloads_info:
                                    self._cdp_downloads_info[guid]["handled"] = True
                            except (KeyError, AttributeError):
                                pass
                            return
                        except Exception as e:
                            self.logger.debug(f"[DownloadsWatchdog] Error checking file {file_path}: {e}")

        self.logger.warning(f"[DownloadsWatchdog] Download did not complete within {max_wait} seconds")

    async def _handle_download(self, download: Any) -> None:
        """Handle a download event."""
        download_id = f"{id(download)}"
        self._active_downloads[download_id] = download
        self.logger.debug(
            f"[DownloadsWatchdog] ⬇️ Handling download: {download.suggested_filename} from {download.url[:100]}..."
        )

        # Debug: Check if download is already being handled elsewhere
        failure = (
            await download.failure()
        )  # TODO: it always fails for some reason, figure out why connect_over_cdp makes accept_downloads not work
        self.logger.warning(f"[DownloadsWatchdog] ❌ Download state - canceled: {failure}, url: {download.url}")
        # logger.info(f'[DownloadsWatchdog] Active downloads count: {len(self._active_downloads)}')

        try:
            current_step = "getting_download_info"
            # Get download info immediately
            url = download.url
            suggested_filename = download.suggested_filename

            current_step = "determining_download_directory"
            # Determine download directory from browser profile
            downloads_dir = self.browser_session.browser_profile.downloads_path
            if not downloads_dir:
                downloads_dir = str(Path.home() / "Downloads")
            else:
                downloads_dir = str(downloads_dir)  # Ensure it's a string

            # Check if Playwright already auto-downloaded the file (due to CDP setup)
            original_path = Path(downloads_dir) / suggested_filename
            if original_path.exists() and original_path.stat().st_size > 0:
                self.logger.debug(
                    f"[DownloadsWatchdog] File already downloaded by Playwright: {original_path} ({original_path.stat().st_size} bytes)"
                )

                # Use the existing file instead of creating a duplicate
                download_path = original_path
                file_size = original_path.stat().st_size
                unique_filename = suggested_filename
            else:
                current_step = "generating_unique_filename"
                # Ensure unique filename
                unique_filename = await self._get_unique_filename(downloads_dir, suggested_filename)
                download_path = Path(downloads_dir) / unique_filename

                self.logger.debug(f"[DownloadsWatchdog] Download started: {unique_filename} from {url[:100]}...")

                current_step = "calling_save_as"
                # Save the download using Playwright's save_as method
                self.logger.debug(f"[DownloadsWatchdog] Saving download to: {download_path}")
                self.logger.debug(f"[DownloadsWatchdog] Download path exists: {download_path.parent.exists()}")
                self.logger.debug(
                    f"[DownloadsWatchdog] Download path writable: {os.access(download_path.parent, os.W_OK)}"
                )

                try:
                    self.logger.debug("[DownloadsWatchdog] About to call download.save_as()...")
                    await download.save_as(str(download_path))
                    self.logger.debug(f"[DownloadsWatchdog] Successfully saved download to: {download_path}")
                    current_step = "save_as_completed"
                except Exception as save_error:
                    self.logger.error(f"[DownloadsWatchdog] save_as() failed with error: {save_error}")
                    raise save_error

                # Get file info
                file_size = download_path.stat().st_size if download_path.exists() else 0

            # Determine file type from extension
            file_ext = download_path.suffix.lower().lstrip(".")
            file_type = file_ext if file_ext else None

            # Try to get MIME type from response headers if available
            mime_type = None
            # Note: Playwright doesn't expose response headers directly from Download object

            # Check if this was a PDF auto-download
            auto_download = False
            if file_type == "pdf":
                auto_download = self._is_auto_download_enabled()

            # Emit download event
            self.event_bus.dispatch(
                FileDownloadedEvent(
                    url=url,
                    path=str(download_path),
                    file_name=suggested_filename,
                    file_size=file_size,
                    file_type=file_type,
                    mime_type=mime_type,
                    from_cache=False,
                    auto_download=auto_download,
                )
            )

            self.logger.debug(
                f"[DownloadsWatchdog] ✅ Download completed: {suggested_filename} ({file_size} bytes) saved to {download_path}"
            )

            # File is now tracked on filesystem, no need to track in memory

        except Exception as e:
            self.logger.error(
                f'[DownloadsWatchdog] Error handling download at step "{locals().get("current_step", "unknown")}", error: {e}'
            )
            self.logger.error(
                f"[DownloadsWatchdog] Download state - URL: {download.url}, filename: {download.suggested_filename}"
            )
        finally:
            # Clean up tracking
            if download_id in self._active_downloads:
                del self._active_downloads[download_id]

    async def check_for_pdf_viewer(self, target_id: TargetID) -> bool:
        """Check if the current target is a PDF using network-based detection.

        This method avoids JavaScript execution that can crash WebSocket connections.
        Returns True if a PDF is detected and should be downloaded.
        """
        self.logger.debug(f"[DownloadsWatchdog] Checking if target {target_id} is PDF viewer...")

        # Use safe API - focus=False to avoid changing focus during PDF check
        try:
            session = await self.browser_session.get_or_create_cdp_session(target_id, focus=False)
        except ValueError as e:
            self.logger.warning(f"[DownloadsWatchdog] No session found for {target_id}: {e}")
            return False

        # Get URL from target
        target = self.browser_session.session_manager.get_target(target_id)
        if not target:
            self.logger.warning(f"[DownloadsWatchdog] No target found for {target_id}")
            return False
        page_url = target.url

        # Check cache first
        if page_url in self._pdf_viewer_cache:
            cached_result = self._pdf_viewer_cache[page_url]
            self.logger.debug(f"[DownloadsWatchdog] Using cached PDF check result for {page_url}: {cached_result}")
            return cached_result

        try:
            # Method 1: Check URL patterns (fastest, most reliable)
            url_is_pdf = self._check_url_for_pdf(page_url)
            if url_is_pdf:
                self.logger.debug(f"[DownloadsWatchdog] PDF detected via URL pattern: {page_url}")
                self._pdf_viewer_cache[page_url] = True
                return True
            chrome_pdf_viewer = self._is_chrome_pdf_viewer_url(page_url)
            if chrome_pdf_viewer:
                self.logger.debug(f"[DownloadsWatchdog] Chrome PDF viewer detected: {page_url}")
                self._pdf_viewer_cache[page_url] = True
                return True

            # Not a PDF
            self._pdf_viewer_cache[page_url] = False
            return False

        except Exception as e:
            self.logger.warning(f"[DownloadsWatchdog] ❌ Error checking for PDF viewer: {e}")
            self._pdf_viewer_cache[page_url] = False
            return False

    def _check_url_for_pdf(self, url: str) -> bool:
        """Check if URL indicates a PDF file."""
        if not url:
            return False

        url_lower = url.lower()

        # Direct PDF file extensions
        if url_lower.endswith(".pdf"):
            return True

        # PDF in path
        if ".pdf" in url_lower:
            return True

        # PDF MIME type in URL parameters
        if any(
            param in url_lower
            for param in [
                "content-type=application/pdf",
                "content-type=application%2fpdf",
                "mimetype=application/pdf",
                "type=application/pdf",
            ]
        ):
            return True

        return False

    def _is_chrome_pdf_viewer_url(self, url: str) -> bool:
        """Check if this is Chrome's internal PDF viewer URL."""
        if not url:
            return False

        url_lower = url.lower()

        # Chrome PDF viewer uses chrome-extension:// URLs
        if "chrome-extension://" in url_lower and "pdf" in url_lower:
            return True

        # Chrome PDF viewer internal URLs
        if url_lower.startswith("chrome://") and "pdf" in url_lower:
            return True

        return False

    async def _check_network_headers_for_pdf(self, target_id: TargetID) -> bool:
        """Infer PDF via navigation history/URL; headers are not available post-navigation in this context."""
        try:
            import asyncio

            # Get CDP session
            temp_session = await self.browser_session.get_or_create_cdp_session(target_id, focus=False)

            # Get navigation history to find the main resource
            history = await asyncio.wait_for(
                temp_session.cdp_client.send.Page.getNavigationHistory(session_id=temp_session.session_id), timeout=3.0
            )

            current_entry = history.get("entries", [])
            if current_entry:
                current_index = history.get("currentIndex", 0)
                if 0 <= current_index < len(current_entry):
                    current_url = current_entry[current_index].get("url", "")

                    # Check if the URL itself suggests PDF
                    if self._check_url_for_pdf(current_url):
                        return True

            # Note: CDP doesn't easily expose response headers for completed navigations
            # For more complex cases, we'd need to set up Network.responseReceived listeners
            # before navigation, but that's overkill for most PDF detection cases

            return False

        except Exception as e:
            self.logger.debug(f"[DownloadsWatchdog] Network headers check failed (non-critical): {e}")
            return False

    async def trigger_pdf_download(self, target_id: TargetID) -> str | None:
        """Trigger download of a PDF from Chrome's PDF viewer.

        Returns the download path if successful, None otherwise.
        """
        self.logger.debug(f"[DownloadsWatchdog] trigger_pdf_download called for target_id={target_id}")

        if not self.browser_session.browser_profile.downloads_path:
            self.logger.warning("[DownloadsWatchdog] ❌ No downloads path configured, cannot save PDF download")
            return None

        downloads_path = self.browser_session.browser_profile.downloads_path
        self.logger.debug(f"[DownloadsWatchdog] Downloads path: {downloads_path}")

        try:
            # Create a temporary CDP session for this target without switching focus
            import asyncio

            self.logger.debug(f"[DownloadsWatchdog] Creating CDP session for PDF download from target {target_id}")
            temp_session = await self.browser_session.get_or_create_cdp_session(target_id, focus=False)

            # Try to get the PDF URL with timeout
            result = await asyncio.wait_for(
                temp_session.cdp_client.send.Runtime.evaluate(
                    params={
                        "expression": """
				(() => {
					// For Chrome's PDF viewer, the actual URL is in window.location.href
					// The embed element's src is often "about:blank"
					const embedElement = document.querySelector('embed[type="application/x-google-chrome-pdf"]') ||
										document.querySelector('embed[type="application/pdf"]');
					if (embedElement) {
						// Chrome PDF viewer detected - use the page URL
						return { url: window.location.href };
					}
					// Fallback to window.location.href anyway
					return { url: window.location.href };
				})()
				""",
                        "returnByValue": True,
                    },
                    session_id=temp_session.session_id,
                ),
                timeout=5.0,  # 5 second timeout to prevent hanging
            )
            pdf_info = result.get("result", {}).get("value", {})

            pdf_url = pdf_info.get("url", "")
            if not pdf_url:
                self.logger.warning(f"[DownloadsWatchdog] ❌ Could not determine PDF URL for download {pdf_info}")
                return None

            # Generate filename from URL
            pdf_filename = os.path.basename(pdf_url.split("?")[0])  # Remove query params
            if not pdf_filename or not pdf_filename.endswith(".pdf"):
                parsed = urlparse(pdf_url)
                pdf_filename = os.path.basename(parsed.path) or "document.pdf"
                if not pdf_filename.endswith(".pdf"):
                    pdf_filename += ".pdf"

            self.logger.debug(f"[DownloadsWatchdog] Generated filename: {pdf_filename}")

            # Check if already downloaded in this session
            self.logger.debug(f"[DownloadsWatchdog] PDF_URL: {pdf_url}, session_pdf_urls: {self._session_pdf_urls}")
            if pdf_url in self._session_pdf_urls:
                existing_path = self._session_pdf_urls[pdf_url]
                self.logger.debug(f"[DownloadsWatchdog] PDF already downloaded in session: {existing_path}")
                return existing_path

            # Generate unique filename if file exists from previous run
            downloads_dir = str(self.browser_session.browser_profile.downloads_path)
            os.makedirs(downloads_dir, exist_ok=True)
            final_filename = pdf_filename
            existing_files = os.listdir(downloads_dir)
            if pdf_filename in existing_files:
                # Generate unique name with (1), (2), etc.
                base, ext = os.path.splitext(pdf_filename)
                counter = 1
                while f"{base} ({counter}){ext}" in existing_files:
                    counter += 1
                final_filename = f"{base} ({counter}){ext}"
                self.logger.debug(f"[DownloadsWatchdog] File exists, using: {final_filename}")

            self.logger.debug(f"[DownloadsWatchdog] Starting PDF download from: {pdf_url[:100]}...")

            # Download using JavaScript fetch to leverage browser cache
            try:
                # Properly escape the URL to prevent JavaScript injection
                escaped_pdf_url = json.dumps(pdf_url)

                result = await asyncio.wait_for(
                    temp_session.cdp_client.send.Runtime.evaluate(
                        params={
                            "expression": f"""
					(async () => {{
						try {{
							// Use fetch with cache: 'force-cache' to prioritize cached version
							const response = await fetch({escaped_pdf_url}, {{
								cache: 'force-cache'
							}});
							if (!response.ok) {{
								throw new Error(`HTTP error! status: ${{response.status}}`);
							}}
							const blob = await response.blob();
							const arrayBuffer = await blob.arrayBuffer();
							const uint8Array = new Uint8Array(arrayBuffer);
							
							// Check if served from cache
							const fromCache = response.headers.has('age') || 
											 !response.headers.has('date');
											 
							return {{ 
								data: Array.from(uint8Array),
								fromCache: fromCache,
								responseSize: uint8Array.length,
								transferSize: response.headers.get('content-length') || 'unknown'
							}};
						}} catch (error) {{
							throw new Error(`Fetch failed: ${{error.message}}`);
						}}
					}})()
					""",
                            "awaitPromise": True,
                            "returnByValue": True,
                        },
                        session_id=temp_session.session_id,
                    ),
                    timeout=10.0,  # 10 second timeout for download operation
                )
                download_result = result.get("result", {}).get("value", {})

                if download_result and download_result.get("data") and len(download_result["data"]) > 0:
                    # Ensure downloads directory exists
                    downloads_dir = str(self.browser_session.browser_profile.downloads_path)
                    os.makedirs(downloads_dir, exist_ok=True)
                    download_path = os.path.join(downloads_dir, final_filename)

                    # Save the PDF asynchronously
                    async with await anyio.open_file(download_path, "wb") as f:
                        await f.write(bytes(download_result["data"]))

                    # Verify file was written successfully
                    if os.path.exists(download_path):
                        actual_size = os.path.getsize(download_path)
                        self.logger.debug(
                            f"[DownloadsWatchdog] PDF file written successfully: {download_path} ({actual_size} bytes)"
                        )
                    else:
                        self.logger.error(f"[DownloadsWatchdog] ❌ Failed to write PDF file to: {download_path}")
                        return None

                    # Log cache information
                    cache_status = "from cache" if download_result.get("fromCache") else "from network"
                    response_size = download_result.get("responseSize", 0)
                    self.logger.debug(
                        f"[DownloadsWatchdog] ✅ Auto-downloaded PDF ({cache_status}, {response_size:,} bytes): {download_path}"
                    )

                    # Store URL->path mapping for this session
                    self._session_pdf_urls[pdf_url] = download_path

                    # Emit file downloaded event
                    self.logger.debug(f"[DownloadsWatchdog] Dispatching FileDownloadedEvent for {final_filename}")
                    self.event_bus.dispatch(
                        FileDownloadedEvent(
                            url=pdf_url,
                            path=download_path,
                            file_name=final_filename,
                            file_size=response_size,
                            file_type="pdf",
                            mime_type="application/pdf",
                            from_cache=download_result.get("fromCache", False),
                            auto_download=True,
                        )
                    )

                    # No need to detach - session is cached
                    return download_path
                else:
                    self.logger.warning(f"[DownloadsWatchdog] No data received when downloading PDF from {pdf_url}")
                    return None

            except Exception as e:
                self.logger.warning(
                    f"[DownloadsWatchdog] Failed to auto-download PDF from {pdf_url}: {type(e).__name__}: {e}"
                )
                return None

        except TimeoutError:
            self.logger.debug("[DownloadsWatchdog] PDF download operation timed out")
            return None
        except Exception as e:
            self.logger.error(f"[DownloadsWatchdog] Error in PDF download: {type(e).__name__}: {e}")
            return None

    @staticmethod
    async def _get_unique_filename(directory: str, filename: str) -> str:
        """Generate a unique filename for downloads by appending (1), (2), etc., if a file already exists."""
        base, ext = os.path.splitext(filename)
        counter = 1
        new_filename = filename
        while os.path.exists(os.path.join(directory, new_filename)):
            new_filename = f"{base} ({counter}){ext}"
            counter += 1
        return new_filename


# Fix Pydantic circular dependency - this will be called from session.py after BrowserSession is defined
