"""The debug window keeps its OWN Control Room session. Nothing is shared.

Why there is no "share the API session" mode
--------------------------------------------
A360's session is the auth token. A window carrying the API's token would not
merely *show* the API's session — it would BE it:

    logout in window      -> the API session dies; every later MCP call returns 401
    token refresh         -> the window's loaded page and the API disagree about
                             which token is current
    multiple-login churn  -> each tab navigation can re-kick login, racing the
                             API's own refresh

The mode was considered and deliberately not built. Guarding a shared session
with warnings still leaves the failure reachable; giving the window its own
session removes it. The window signs itself in (the user types credentials
once; the persistent Chromium profile's remembered-device cookie makes later
refreshes silent).

The cost is **one extra session slot** out of the account's five. This module
accounts for it on the same disk ledger the AuthManager uses, so the budget
stays honest across processes. When the budget is full the launch is refused
up-front rather than letting the new browser evict an existing session.
"""

from __future__ import annotations

import logging

from ..auth._paths import account_key
from ..auth._session_budget import (
    MAX_CONCURRENT_SESSIONS,
    SessionBudget,
    SessionBudgetExceeded,
)

logger = logging.getLogger(__name__)

# Distinct owner label so the ledger can tell the browser slot apart from the
# API slot held by AuthManager (whose owner defaults to "a360-mcp:<pid>").
BROWSER_OWNER_PREFIX = "a360-mcp:browser"

# How much headroom to leave when claiming a browser slot. Default 1: we need
# at least one free slot to launch the window without evicting anyone.
RESERVED_HEADROOM = 1


def browser_owner(username: str) -> str:
    """Stable owner label for the browser slot tied to ``username``."""
    return f"{BROWSER_OWNER_PREFIX}:{username}"


def claim_browser_slot(*, control_room_url: str, username: str, now: float | None = None) -> None:
    """Reserve a session slot for the debug window, or raise if none is free.

    Raises :class:`SessionBudgetExceeded` when claiming would leave less than
    :data:`RESERVED_HEADROOM` slots free, so the user always has at least one
    slot in hand for other work. Safe to call repeatedly: re-claiming the same
    owner renews the slot instead of consuming a second one.
    """
    key = account_key(control_room_url, username)
    budget = SessionBudget(key)
    summary = budget.summary(now=now)
    used = int(summary.get("used", 0))
    limit = int(summary.get("limit", MAX_CONCURRENT_SESSIONS))
    if used + RESERVED_HEADROOM >= limit:
        holders = summary.get("owners", [])
        raise SessionBudgetExceeded(
            f"Opening the debug window would consume session slot {used + 1}/{limit}, "
            f"leaving less than {RESERVED_HEADROOM} free. The window keeps its OWN "
            "Control Room session (it does not share the API's), and a sixth login "
            "evicts an existing session. Close another session first. "
            f"Current holders: {', '.join(holders) or '(none recorded)'}"
        )
    budget.claim(browser_owner(username), now=now)
    logger.info(
        "Claimed browser session slot for %s@%s (now %s/%s)",
        username,
        control_room_url,
        used + 1,
        limit,
    )


def bind_browser_slot(
    *, control_room_url: str, username: str, pid: int, now: float | None = None
) -> None:
    """Re-stamp the browser slot with the launched window's own pid.

    The slot has to be claimed *before* the launch (so a full budget refuses
    the window instead of evicting someone), which means it is first recorded
    against the MCP server's pid. Chromium is started detached and outlives the
    server, so leaving it that way makes the slot read as stale on the next
    server restart while the window's Control Room session is still live.

    Best-effort by design: the window is already running by the time this is
    called, so a ledger problem must not become a reason to fail the launch.
    """
    key = account_key(control_room_url, username)
    try:
        SessionBudget(key).claim(browser_owner(username), now=now, pid=pid)
    except (SessionBudgetExceeded, OSError):
        logger.warning(
            "Could not bind the browser session slot to pid %s; it stays stamped with "
            "this process and may be reaped early",
            pid,
            exc_info=True,
        )


def release_browser_slot(*, control_room_url: str, username: str) -> None:
    """Release the browser slot. Safe to call when nothing is held."""
    key = account_key(control_room_url, username)
    SessionBudget(key).release(browser_owner(username))
    logger.debug("Released browser session slot for %s@%s", username, control_room_url)


def browser_session_status(
    *, control_room_url: str, username: str, now: float | None = None
) -> dict:
    """Report whether the browser slot is currently held, for diagnostics."""
    key = account_key(control_room_url, username)
    owner = browser_owner(username)
    summary = SessionBudget(key).summary(now=now)
    return {
        "held": owner in summary.get("owners", []),
        "owner": owner,
        "used": summary.get("used", 0),
        "limit": summary.get("limit", MAX_CONCURRENT_SESSIONS),
        "all_holders": summary.get("owners", []),
    }


__all__ = [
    "BROWSER_OWNER_PREFIX",
    "RESERVED_HEADROOM",
    "bind_browser_slot",
    "browser_owner",
    "browser_session_status",
    "claim_browser_slot",
    "release_browser_slot",
]
