"""Entrypoint for the browser service: ``python -m abstra_internals.cloud.browser_service``.

Mirrors ``abstra_internals.cloud.worker``: init tracing/logging, then consume
RabbitMQ. Unlike the per-project Selenium the worker/editor use, the service
launches its OWN Chromium locally in its pod — so ``SELENIUM_REMOTE_URL`` is
deliberately left UNSET here, and ``BrowserTools`` launches an in-pod browser
(no Grid, no per-project targeting). That's what keeps the service shared,
EFS-less, and free of the cross-cluster Selenium problem.
"""

import os
import threading
import time
from typing import Optional

from abstra_internals.cloud.tracing import init_tracing

# Must run before pika/playwright are imported so instrumentation can patch them.
init_tracing(service_name="abstra-browser-service")

from abstra_internals.cloud.browser_service.pool import (  # noqa: E402
    BrowserSessionPool,
)
from abstra_internals.cloud.browser_service.transport import (  # noqa: E402
    BrowserServer,
)
from abstra_internals.environment import (  # noqa: E402
    NATS_CREDS,
    NATS_URL,
    RABBITMQ_CONNECTION_URI,
)
from abstra_internals.logger import AbstraLogger  # noqa: E402
from abstra_internals.signals import SignalHandlers  # noqa: E402

_EVICT_SWEEP_SECONDS = 30.0
_MAX_SESSIONS = int(os.getenv("BROWSER_SERVICE_MAX_SESSIONS", "6"))
# How long an idle session keeps its browser (and its serve thread waiting on
# the NATS cmd subject). Must cover a long human-approval wait — a command
# arriving after the pool discarded the browser fails with a stale-tab error
# the dead-session retry deliberately does not catch. Sessions have no broker
# queues anymore, so this is purely the Chromium-retention budget.
_IDLE_TTL_SECONDS = float(os.getenv("BROWSER_SERVICE_IDLE_TTL_SECONDS", "7500"))


def _make_browser_tools(project_id: str, editor_token: Optional[str]):
    """Real BrowserTools factory: an in-pod Chromium (no Selenium). project_id /
    editor_token are threaded through for open_page (Phase 4) but unused by the
    13 generic browser tools."""
    from abstra_internals.agents.tools.browser import BrowserTools

    return BrowserTools(listen_network=True, listen_console=True)


def _start_evict_sweeper(pool: BrowserSessionPool, stop: threading.Event) -> None:
    def loop() -> None:
        while not stop.wait(_EVICT_SWEEP_SECONDS):
            try:
                evicted = pool.evict_idle()
                if evicted:
                    AbstraLogger.warning(
                        f"[browser-service] evicted {evicted} idle session(s)"
                    )
            except Exception as e:  # noqa: BLE001
                AbstraLogger.error(f"[browser-service] evict sweep failed: {e}")

    threading.Thread(target=loop, name="browser-service-evict", daemon=True).start()


def _require_chromium_in_image() -> None:
    """Fail fast if Chromium isn't baked into the image. Better a clear boot
    error than BrowserTools running `playwright install chromium` mid-request
    (a 150MB download that fails on a read-only fs). Checks the expected
    executable path only — does not launch a browser."""
    from playwright.sync_api import sync_playwright

    try:
        with sync_playwright() as pw:
            executable = pw.chromium.executable_path
        exists = os.path.exists(executable)
    except Exception as e:  # noqa: BLE001
        raise RuntimeError(
            f"[browser-service] could not resolve the Chromium executable: {e}. "
            "The image must bake it in (see Dockerfile.browser-service)."
        )
    if not exists:
        raise RuntimeError(
            f"[browser-service] Chromium not found at {executable}. The image must "
            "bake it in (playwright install --with-deps chromium); refusing to "
            "install a browser at runtime."
        )


def run() -> None:
    SignalHandlers.init()
    AbstraLogger.init("cloud")
    AbstraLogger.warning(
        f"[browser-service] starting (idle TTL {_IDLE_TTL_SECONDS}s, "
        f"evict sweep {_EVICT_SWEEP_SECONDS}s, max sessions {_MAX_SESSIONS})"
    )

    if not RABBITMQ_CONNECTION_URI:
        raise Exception("RABBITMQ_CONNECTION_URI not found")
    if not NATS_URL or not NATS_CREDS:
        raise Exception("ABSTRA_NATS_URL / ABSTRA_NATS_CREDS not found")

    # SELENIUM_REMOTE_URL must be unset so BrowserTools launches a local browser.
    if os.environ.pop("SELENIUM_REMOTE_URL", None):
        AbstraLogger.warning(
            "[browser-service] cleared inherited SELENIUM_REMOTE_URL; the service uses in-pod Chromium"
        )

    _require_chromium_in_image()

    pool = BrowserSessionPool(_make_browser_tools, idle_ttl_seconds=_IDLE_TTL_SECONDS)
    server = BrowserServer(
        RABBITMQ_CONNECTION_URI,
        pool,
        max_sessions=_MAX_SESSIONS,
        nats_url=NATS_URL,
        nats_creds=NATS_CREDS,
        session_idle_seconds=_IDLE_TTL_SECONDS,
    )

    stop = threading.Event()
    _start_evict_sweeper(pool, stop)

    def _shutdown() -> None:
        stop.set()
        server.stop()
        pool.close_all()

    SignalHandlers.register_sigterm_callback(_shutdown)

    try:
        server.run()
    finally:
        _shutdown()
        # Give session teardowns a beat to flush before the process exits.
        time.sleep(0.2)


if __name__ == "__main__":
    run()
