"""Signed-in state per engine (Pink, 2026-08-10 talk-workspace-context spec,
item 2): detect_engines() (engines.py) is shutil.which() only, so it knows
INSTALLED and not SIGNED IN. Survivable with one bridge-routed provider;
with the talk-key-source selector offering three, an unsigned-in choice
fails on the user's first message with no warning.

Two engines expose a clean, structured status check:
    codex login status   -> "Logged in using ChatGPT" (plain text, exit 0)
    claude auth status   -> {"loggedIn": true, ...} (JSON, exit 0)

antigravity (agy) does not: `agy --help` lists no login/auth-status
subcommand at all (verified 2026-08-10). Rather than guess from an
ambiguous signal, agy always resolves to None (unknown) — the caller
decides how to render that (e.g. "installed" only, no signed-in gate),
but this module never claims a yes/no it cannot back.
"""

from __future__ import annotations

import asyncio
import json
from typing import Optional

from atashi_bridge.engines import detect_engines

_AUTH_CHECK_TIMEOUT_SECONDS = 10

# engine -> the status subcommand to run. Engines absent here (antigravity)
# have no known check and always resolve to None.
_AUTH_COMMANDS = {
    "codex": ("codex", "login", "status"),
    "claude_code": ("claude", "auth", "status"),
}


async def _run_status_command(cmd: tuple[str, ...]) -> Optional[tuple[int, bytes]]:
    """Run cmd, return (returncode, stdout) or None if it couldn't be run at
    all (missing binary, timeout) — never raises."""
    try:
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=_AUTH_CHECK_TIMEOUT_SECONDS)
    except (OSError, asyncio.TimeoutError):
        return None
    return proc.returncode, stdout


def _parse_codex(returncode: int, stdout: bytes) -> Optional[bool]:
    if returncode != 0:
        return False
    # startswith, not a bare substring check: "logged in" is a substring of
    # codex's own "Not logged in" message, which would otherwise read as
    # signed-in. Verified live output: "Logged in using ChatGPT".
    return stdout.decode("utf-8", errors="replace").strip().lower().startswith("logged in")


def _parse_claude(returncode: int, stdout: bytes) -> Optional[bool]:
    """Trust the CLI's OWN answer; fall back to unknown, never to a guessed no.

    ⚠️ THE ORDER HERE IS THE WHOLE POINT. This used to read the RETURN CODE
    first and map any non-zero to ``False`` before even looking at the output.
    That is a confident claim built on a signal that does not carry it:
    ``claude auth status`` exits NON-ZERO WHEN SIGNED OUT (verified live
    2026-08-20: exit 1 with a well-formed ``{"loggedIn": false}`` body), and it
    also exits non-zero when the subcommand does not exist on an older CLI, when
    the config dir is unreadable, and when it crashes. Only the first of those
    means "signed out", and the old order reported all four as an EXPLICIT
    signed-out — which the frontend renders as "I found it but it isn't signed
    in" and tells the user to go and sign in to fix a problem they do not have.

    This module's own docstring already promised it "never claims a yes/no it
    cannot back". This function was the one place that broke that promise.

    Parsing first keeps every TRUE negative — a real signed-out CLI still emits
    ``loggedIn: false`` and still resolves False — while a response we cannot
    read now resolves None (unknown), which callers are already required to
    treat as "do not gate".
    """
    try:
        payload = json.loads(stdout.decode("utf-8", errors="replace"))
    except (json.JSONDecodeError, UnicodeDecodeError):
        # Unparseable. A non-zero exit with no readable body tells us the check
        # failed, NOT that the user is signed out.
        return None
    if not isinstance(payload, dict) or "loggedIn" not in payload:
        return None
    return bool(payload.get("loggedIn"))


_PARSERS = {"codex": _parse_codex, "claude_code": _parse_claude}


async def _check_one(engine: str) -> Optional[bool]:
    cmd = _AUTH_COMMANDS.get(engine)
    if cmd is None:
        return None  # antigravity: no known status command, never guess
    result = await _run_status_command(cmd)
    if result is None:
        return None
    returncode, stdout = result
    return _PARSERS[engine](returncode, stdout)


async def detect_engine_auth(installed: Optional[list[str]] = None) -> dict[str, Optional[bool]]:
    """Signed-in state per installed engine.

    installed: engines to check (from engines.detect_engines() if omitted —
    the live wiring the daemon uses). Only checks engines actually present;
    never fires a subprocess for a binary that isn't on PATH.

    Returns True/False when there's a reliable signal (codex, claude_code),
    None when there isn't (antigravity, or any check that failed to run).
    """
    engines_to_check = detect_engines() if installed is None else installed
    return {engine: await _check_one(engine) for engine in engines_to_check}
