"""Workflow auth-profile resolver.

Calls the gateway's `/api/auth/profiles/{id}/resolve` endpoint to fetch the
decrypted credential bound to a workflow step. Authenticates with the
service token written to ``~/.revka/service-token`` at gateway startup.

Failure modes are mapped to a structured ``auth_resolve_failed`` error so
step executors can fail fast with a meaningful reason instead of leaking
HTTP plumbing into workflow output.

Configuration:
    REVKA_AUTH_RESOLVE_TIMEOUT — float seconds for the urlopen call to
        the gateway resolve endpoint. Defaults to 10.0. Useful in slow CI
        environments or when running through a constrained loopback proxy.
"""
from __future__ import annotations

import asyncio
import json
import os
from typing import Any
from urllib.parse import quote
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError

from .._log import _log


class AuthResolveError(Exception):
    """Raised when an auth profile can't be resolved.

    ``code`` mirrors the gateway error code (``auth_profile_not_found``,
    ``auth_profile_expired``, ``auth_store_unavailable``, etc.) — bubbled
    up to step results as ``auth_resolve_failed: <code>``.
    """

    def __init__(self, message: str, code: str = "auth_resolve_failed") -> None:
        super().__init__(message)
        self.code = code


def _resolve_timeout() -> float:
    """Return the urlopen timeout for the gateway resolve call (seconds)."""
    raw = os.environ.get("REVKA_AUTH_RESOLVE_TIMEOUT", "")
    try:
        value = float(raw) if raw else 10.0
    except ValueError:
        return 10.0
    return value if value > 0 else 10.0


def _gateway_url() -> str:
    """Return the gateway base URL.

    Order: explicit ``REVKA_GATEWAY_URL`` env override → the local
    daemon at 127.0.0.1:60003 (Revka's default port).
    """
    return (
        os.environ.get("REVKA_GATEWAY_URL")
        or os.environ.get("GATEWAY_URL")
        or "http://127.0.0.1:60003"
    ).rstrip("/")


def _service_token() -> str:
    """Read the gateway service token from disk.

    Generated by the Rust gateway at startup. Empty string when the file
    is missing — the resolver will then fail with a clear error rather
    than silently fall back to an unauthenticated call.
    """
    # Allow env override for tests / nonstandard deployments.
    env_tok = os.environ.get("REVKA_SERVICE_TOKEN", "").strip()
    if env_tok:
        return env_tok
    path = os.path.expanduser("~/.revka/service-token")
    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read().strip()
    except FileNotFoundError:
        return ""
    except OSError as exc:
        _log(f"auth_resolver: failed to read service token at {path}: {exc}")
        return ""


def _do_resolve_blocking(profile_id: str) -> dict[str, Any]:
    token = _service_token()
    if not token:
        raise AuthResolveError(
            "service token unavailable — gateway must be running and have written "
            "~/.revka/service-token",
            code="service_token_unavailable",
        )
    url = f"{_gateway_url()}/api/auth/profiles/{quote(profile_id, safe=':')}/resolve"
    req = Request(
        url,
        method="POST",
        data=b"",
        headers={
            "X-Revka-Service-Token": token,
            "Content-Length": "0",
        },
    )
    try:
        with urlopen(req, timeout=_resolve_timeout()) as resp:
            body = resp.read()
            try:
                return json.loads(body.decode("utf-8"))
            except (UnicodeDecodeError, json.JSONDecodeError) as exc:
                raise AuthResolveError(
                    f"malformed resolve response: {exc}",
                    code="auth_resolve_malformed",
                ) from exc
    except HTTPError as exc:
        # Gateway returns structured error JSON for 404/410/etc.
        try:
            err_body = json.loads(exc.read().decode("utf-8"))
        except Exception:  # noqa: BLE001
            err_body = {}
        code = err_body.get("code") or f"http_{exc.code}"
        message = err_body.get("error") or f"resolve failed: HTTP {exc.code}"
        raise AuthResolveError(message, code=code) from exc
    except URLError as exc:
        raise AuthResolveError(
            f"gateway unreachable at {url}: {exc.reason}",
            code="gateway_unreachable",
        ) from exc


async def resolve_auth_profile(profile_id: str) -> dict[str, Any]:
    """Resolve an auth profile id to ``{ token, kind, provider, profile_name, expires_at }``.

    Returns the decrypted credential dict on success. Raises
    :class:`AuthResolveError` (with structured ``code``) on any failure —
    callers should turn this into an ``auth_resolve_failed`` step error.

    The actual HTTP call uses stdlib ``urllib`` and is run in a thread to
    keep the asyncio loop free; httpx is intentionally not used here so
    this module has zero new deps.
    """
    if not profile_id:
        raise AuthResolveError("empty auth profile id", code="auth_profile_id_empty")
    return await asyncio.to_thread(_do_resolve_blocking, profile_id)
