"""CORS static file server for LiveView components.

LiveView components run in the browser and fetch things over HTTP: their
data (Zarr / OME-TIFF), and — for both agent-generated components and viewer
*plugins* — their own JS code. Those files live on local disk (the
workspace, or a skills/plugins directory), which a browser cannot fetch by
path. This lazily starts a localhost, CORS-enabled static HTTP server that
exposes one or more local directories.

The server runs in a **dedicated daemon thread with its own event loop**,
not on the caller's loop: the live_view toolset's tool calls execute under
ThreadJob isolation (ephemeral per-call loops), so a server bound to the
calling loop would die the moment the tool call returns. The daemon thread
keeps the server alive for the whole backend process.

Multiple roots are supported — each is mounted under a short, stable
hash prefix (`/<hash>/...`) so unrelated directory trees (workspace,
skills dir) can be served by one process. Localhost-bound. Supports HTTP
range requests (needed for OME-TIFF / sharded Zarr).
"""

from __future__ import annotations

import asyncio
import hashlib
import socket
import threading
from pathlib import Path

from aiohttp import web

from pantheon.utils.log import logger


def _free_port() -> int:
    s = socket.socket()
    try:
        s.bind(("127.0.0.1", 0))
        return s.getsockname()[1]
    finally:
        s.close()


def _prefix_for(root: Path) -> str:
    """A short, stable URL prefix for a root (order-independent)."""
    return hashlib.sha1(str(root).encode()).hexdigest()[:10]


@web.middleware
async def _cors_middleware(request: web.Request, handler):
    """Allow the LiveView iframe (a different origin/port) to fetch."""
    if request.method == "OPTIONS":
        resp: web.StreamResponse = web.Response()
    else:
        resp = await handler(request)
    resp.headers["Access-Control-Allow-Origin"] = "*"
    resp.headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS"
    resp.headers["Access-Control-Allow-Headers"] = "*"
    return resp


class LiveViewDataServer:
    """Lazily-started localhost CORS static server over one or more roots.

    One server per backend process. Runs in a daemon thread so it outlives
    the tool call that started it.
    """

    def __init__(self):
        self._thread: threading.Thread | None = None
        self._roots: dict[str, Path] = {}  # prefix -> root dir
        self._base_url: str | None = None
        self._lock = threading.Lock()

    async def ensure_started(self, roots: list[Path]) -> str:
        """Start the server (once) serving `roots`; return its base URL.

        The first call fixes the served roots. Started on a dedicated daemon
        thread; this coroutine waits (off the calling loop) for it to come up.
        """
        if self._base_url is not None:
            return self._base_url
        resolved = [Path(r).resolve() for r in roots if Path(r).exists()]
        await asyncio.get_event_loop().run_in_executor(
            None, self._start_blocking, resolved,
        )
        return self._base_url  # type: ignore[return-value]

    def _start_blocking(self, roots: list[Path]) -> None:
        with self._lock:
            if self._base_url is not None:
                return
            ready = threading.Event()
            err: dict[str, BaseException] = {}

            def _run() -> None:
                loop = asyncio.new_event_loop()
                asyncio.set_event_loop(loop)
                try:
                    app = web.Application(middlewares=[_cors_middleware])
                    mounts: dict[str, Path] = {}
                    for root in roots:
                        prefix = _prefix_for(root)
                        if prefix in mounts:
                            continue
                        mounts[prefix] = root
                        app.router.add_static(
                            f"/{prefix}/", str(root),
                            show_index=False, follow_symlinks=False,
                        )
                    runner = web.AppRunner(app)
                    loop.run_until_complete(runner.setup())
                    port = _free_port()
                    site = web.TCPSite(runner, "127.0.0.1", port)
                    loop.run_until_complete(site.start())
                    self._roots = mounts
                    self._base_url = f"http://127.0.0.1:{port}"
                except BaseException as e:  # noqa: BLE001
                    err["e"] = e
                finally:
                    ready.set()
                if "e" not in err:
                    loop.run_forever()  # keep the server alive

            self._thread = threading.Thread(
                target=_run, daemon=True, name="live-view-data-server",
            )
            self._thread.start()
            if not ready.wait(timeout=10):
                raise RuntimeError("LiveView data server failed to start (timeout)")
            if "e" in err:
                raise err["e"]
            logger.info(
                "live_view: data server at {} serving {} root(s)",
                self._base_url, len(self._roots),
            )

    @property
    def base_url(self) -> str | None:
        return self._base_url

    @property
    def roots(self) -> list[Path]:
        return list(self._roots.values())

    def url_for(self, abs_path: Path) -> str | None:
        """Map an absolute path under any served root to its URL."""
        if self._base_url is None:
            return None
        target = Path(abs_path).resolve()
        for prefix, root in self._roots.items():
            try:
                rel = target.relative_to(root)
            except ValueError:
                continue
            return f"{self._base_url}/{prefix}/{rel.as_posix()}"
        return None
