"""The HTTP layer — every command ends up here.

A tiny wrapper over httpx that attaches the bearer token + workspace header and
turns non-2xx responses into a clean ApiError (so the CLI can print the server's
message and exit 1, the same branch contract as ./r's REJECTED).
"""
import httpx

from . import config


class ApiError(Exception):
    def __init__(self, message, status=0, detail=None):
        self.status, self.detail = status, detail or {}
        super().__init__(message)


def _headers(auth=True):
    h = {"Content-Type": "application/json"}
    if auth:
        tok = config.token()
        if not tok:
            raise ApiError("not signed in — run `runner signin`", 401)
        h["Authorization"] = "Bearer " + tok
        ws = config.workspace()
        if ws:
            h["X-Runner-Workspace"] = ws
    return h


def _base():
    return config.api_base()


def _check(resp):
    if resp.status_code // 100 == 2:
        return resp.json() if resp.content else {}
    try:
        body = resp.json()
        msg = body.get("error") or body.get("detail") or resp.text
    except Exception:
        body, msg = {}, resp.text
    raise ApiError(msg, resp.status_code, body)


def post_op(op, args, *, timeout=120):
    """The one authoring call: POST /api/v1/op {op, args}."""
    with httpx.Client(timeout=timeout) as c:
        r = c.post(_base() + "/api/v1/op", json={"op": op, "args": args}, headers=_headers())
    return _check(r)


def get(path, params=None, *, auth=True, timeout=60):
    with httpx.Client(timeout=timeout) as c:
        r = c.get(_base() + path, params=params, headers=_headers(auth))
    return _check(r)


def post(path, body=None, *, auth=True, timeout=60):
    with httpx.Client(timeout=timeout) as c:
        r = c.post(_base() + path, json=body or {}, headers=_headers(auth))
    return _check(r)


def delete(path, *, timeout=60):
    with httpx.Client(timeout=timeout) as c:
        r = c.delete(_base() + path, headers=_headers())
    return _check(r)


# --- surface access (runtime routes: /bundle, /read, /run, /feed) ----------
# These routes are COOKIE-authenticated, not bearer-authenticated. We trade the
# signed-in bearer for a per-app session cookie via /api/v1/dev/session (the
# same bridge `runner dev` used) and attach it directly — no localhost proxy,
# because an agent is an HTTP client and can set its own Cookie header. The
# cookie is scoped to ONE in-tenant app, so it's least-privilege; the bearer the
# CLI already holds is strictly more powerful, so this widens nothing.
_SESS = {}  # scope -> {"cookie": "name=value", "prefix": str}


def surface_session(scope, *, refresh=False):
    """Mint (and cache) a runtime session cookie scoped to one app scope."""
    if not scope:
        raise ApiError("surface access needs an app scope")
    if refresh or scope not in _SESS:
        s = post("/api/v1/dev/session", {"scope": scope})
        _SESS[scope] = {"cookie": f"{s['cookie_name']}={s['cookie_value']}",
                        "prefix": s.get("prefix", "")}
    return _SESS[scope]


def surface_request(method, scope, path, *, params=None, json=None, timeout=60):
    """Call a cookie-authed runtime route for an app as the signed-in user.
    Re-mints the session once on a 401 (expired/rotated cookie)."""
    params = dict(params or {})
    params.setdefault("scope", scope)
    with httpx.Client(timeout=timeout) as c:
        def _do(sess):
            return c.request(method, _base() + sess["prefix"] + path,
                             params=params, json=json,
                             headers={"Cookie": sess["cookie"]})
        r = _do(surface_session(scope))
        if r.status_code == 401:
            r = _do(surface_session(scope, refresh=True))
    return _check(r)


def surface_get(scope, path, params=None, **kw):
    """GET a runtime route, e.g. surface_get('acme.crm', '/bundle')."""
    return surface_request("GET", scope, path, params=params, **kw)


# --- device flow (used by signin; no bearer yet) ---------------------------
def device_start(token_name="cli"):
    with httpx.Client(timeout=30) as c:
        r = c.post(_base() + "/api/v1/auth/device/start", json={"token_name": token_name},
                   headers={"Content-Type": "application/json"})
    return _check(r)


def device_token(device_code):
    """Poll the grant. Returns (status_code, body) WITHOUT raising, because the
    poll's 202/410/403 are control signals, not errors."""
    with httpx.Client(timeout=30) as c:
        r = c.post(_base() + "/api/v1/auth/device/token", json={"device_code": device_code},
                   headers={"Content-Type": "application/json"})
    try:
        return r.status_code, r.json()
    except Exception:
        return r.status_code, {}
