"""Per-run credential for the W4P activation call made from a project's GitHub Actions run.

The workflow requests a GitHub OIDC token for the run. Core accepts it only when GitHub signed it,
the audience is this activation audience, and the claims name the project's repository and the
exact commit being deployed. A token is good for one run, so there is no long-lived secret in the repo.
"""

from __future__ import annotations

from collections.abc import Awaitable, Callable
from typing import Any

from jose import jwt
from jose.exceptions import JWTError

GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"
# Deployment state the deploy task leaves behind while it waits for the run to activate.
WAITING_FOR_CI_STEP = "waiting_for_ci_activation"
GITHUB_OIDC_JWKS_URL = "https://token.actions.githubusercontent.com/.well-known/jwks"


class CiActivationError(RuntimeError):
    def __init__(self, code: str) -> None:
        super().__init__(code)
        self.code = code


def verify_ci_run_token(
    token: str,
    *,
    audience: str,
    jwks: dict[str, Any],
    expected_repository: str,
    expected_sha: str | None = None,
) -> dict[str, Any]:
    """Return the verified claims, or raise CiActivationError. Fails closed on any mismatch.

    With ``expected_sha`` the token must name that commit. Without it, the commit comes from the
    token, and the token must carry one."""
    if not audience:
        raise CiActivationError("activation_audience_not_configured")
    if not expected_repository:
        raise CiActivationError("expected_run_unknown")
    try:
        claims = jwt.decode(
            token,
            jwks,
            algorithms=["RS256"],
            audience=audience,
            issuer=GITHUB_OIDC_ISSUER,
        )
    except JWTError as exc:
        raise CiActivationError("invalid_ci_token") from exc
    if (claims.get("repository") or "").lower() != expected_repository.lower():
        raise CiActivationError("repository_mismatch")
    if expected_sha is None:
        if not claims.get("sha"):
            raise CiActivationError("sha_missing")
        return claims
    if (claims.get("sha") or "").lower() != expected_sha.lower():
        raise CiActivationError("sha_mismatch")
    return claims


async def fetch_github_jwks(get: Callable[[str], Awaitable[dict[str, Any]]]) -> dict[str, Any]:
    """Load GitHub's signing keys. ``get`` is injected so tests never reach the network."""
    return await get(GITHUB_OIDC_JWKS_URL)
