// Package auth — mint_sme.go: cross-realm HS256 mint helper that
// bridges a Keycloak-issued RS256 operator session into the
// SME-microservice HS256 wire contract.
//
// ── Why this file exists ─────────────────────────────────────────────
//
// catalyst-api authenticates operators via Keycloak (RS256, JWKS). The
// SME microservice mesh (core/services/{gateway,billing,catalog,...})
// validates HS256 tokens signed with `sme-secrets/JWT_SECRET`. The two
// secret-spaces are different by design — Keycloak rotates its RSA
// keypair on its own schedule and the SME mesh's symmetric secret is
// auto-generated by the chart's `sme-secrets` template with persistent
// `helm.sh/resource-policy: keep` semantics.
//
// catalyst-api's SME proxies (sme_billing_vouchers.go and siblings)
// were originally written to forward the operator's RS256 Authorization
// header verbatim to the SME gateway. The gateway's `validateJWT`
// (core/services/gateway/proxy.go) rejects RS256 outright — it only
// accepts `*jwt.SigningMethodHMAC`. Result on a fresh Sovereign:
// every operator click on /bss/vouchers → catalyst-api → SME gateway
// returns 401 with no per-operator audit trail upstream.
//
// MintSMEAccessToken is the bridge: given the operator's
// Keycloak-derived identity (`sub`, `email`, `role` after the
// realm-role → SME-vocabulary mapping done by SMERoleFor below) and
// the HS256 secret pulled from the cluster's `sme-secrets` Secret,
// emit a fresh short-lived HS256 token the SME gateway will accept.
// The minted token is intended to be forwarded as the `Authorization:
// Bearer …` header on the very next upstream hop; it MUST NOT be
// persisted (per docs/INVIOLABLE-PRINCIPLES.md #10 — credentials live
// only in transit).
//
// ── Claim shape ──────────────────────────────────────────────────────
//
// The minted JWT carries the minimum the SME gateway's `validateJWT`
// + the billing service's `requireVoucherIssuer` (and equivalent
// gates) require:
//
//	sub   — operator's Keycloak `sub` (UUID). Forwarded into the
//	        upstream `X-User-ID` header by the gateway.
//	email — operator's primary email (mirrors core/services/auth's
//	        issueTokens). Useful for upstream audit attribution.
//	role  — SME-vocabulary role: "superadmin" | "sovereign-admin" |
//	        "member". Mapped from the Keycloak realm-role +
//	        tier-claim shape by SMERoleFor.
//	iat / exp — issued-at + 5-minute expiry. Short-lived because
//	        we mint per-hop; a stolen token has minimum blast radius.
//	typ   — "session" (matches the auth-service issueTokens
//	        discriminator so any upstream audit trail can tell
//	        bridged-from-RS256 tokens apart from PATs).
//
// Compatible with core/services/shared/auth.Claims — adding new fields
// (e.g. org_id once the Wave 1b mapper lands) is a one-line edit here
// because the underlying jwt.MapClaims is a free-form map.
//
// ── Why this lives in shared/auth ────────────────────────────────────
//
// Future bridges (sandbox-controller → SME, MCP server → SME) need
// the same RS256 → HS256 hop. Hoisting the mint helper out of
// catalyst-api keeps the shape consistent and prevents per-consumer
// drift. The helper has no IO, no clock dependency injection, and no
// state — it's a pure function over (secret, claims) and trivial to
// unit-test (mint_sme_test.go in this same dir).
package auth

import (
	"fmt"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

// SMETokenTTL is the lifetime of a bridged HS256 token. Short by
// design — every upstream hop mints a fresh token from the operator's
// already-validated RS256 session, so the bridged token never lives
// outside the catalyst-api → SME gateway TCP path. 5 minutes covers
// the slowest reasonable proxy round-trip with comfortable headroom
// for a stalled upstream; a stolen token has minimum blast radius.
const SMETokenTTL = 5 * time.Minute

// MintSMEAccessToken mints an HS256-signed JWT the SME gateway +
// downstream services (billing / catalog / tenant / …) will accept.
//
// `secret` is the raw bytes of `sme-secrets/JWT_SECRET` (NOT the
// base64-encoded form — that's the Secret-storage shape; the Pod env
// already holds the decoded plaintext bytes).
//
// `sub`, `email`, `role` come from the operator's Keycloak-derived
// session claims. The caller is responsible for running SMERoleFor()
// on the realm-roles → role string before passing it in.
//
// Returns the compact JWT serialization (header.payload.sig) ready
// to be set as `Authorization: Bearer <token>` on the upstream
// request. Empty `secret` is rejected with an error rather than
// silently producing a forgeable token — the caller MUST surface a
// 500 / 503 to the operator so the chart-wiring gap is observable.
func MintSMEAccessToken(secret []byte, sub, email, role string) (string, error) {
	if len(secret) == 0 {
		return "", fmt.Errorf("auth: SME JWT secret empty (chart not wired with sme-secrets/JWT_SECRET)")
	}
	now := time.Now()
	claims := jwt.MapClaims{
		"sub":   sub,
		"email": email,
		"role":  role,
		"iat":   now.Unix(),
		"exp":   now.Add(SMETokenTTL).Unix(),
		"typ":   "session",
	}
	tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	out, err := tok.SignedString(secret)
	if err != nil {
		return "", fmt.Errorf("auth: sign SME bridge token: %w", err)
	}
	return out, nil
}

// SMERoleFor projects a Keycloak operator identity onto the
// SME-microservice role vocabulary that core/services/billing's
// `requireVoucherIssuer` (and equivalent gates across catalog /
// tenant / provisioning) expects.
//
// SME-vocabulary roles (canonical, from core/services/billing/handlers
// /handlers.go requireVoucherIssuer):
//
//	"superadmin"      — direct OpenOva operations (Catalyst-Zero
//	                    operators). Mapped from the catalyst realm's
//	                    `catalyst-owner` role.
//	"sovereign-admin" — franchisee operations on a franchised
//	                    Sovereign. Mapped from `catalyst-admin` or
//	                    the legacy `sovereign-admin` realm-role
//	                    + the `admin`/`owner` tier claim
//	                    (matches policyModeCallerAuthorized in
//	                    products/catalyst/bootstrap/api/internal
//	                    /handler/policy_mode.go).
//	"member"          — every other authenticated operator. Read-only
//	                    SME endpoints (list catalog, view own orders)
//	                    accept this; privileged surfaces 403 upstream
//	                    so the caller sees a real authz error rather
//	                    than the silent 401 the pre-bridge state
//	                    produced.
//
// `realmRoles` is the bearer's realm-role list (Keycloak
// `realm_access.roles`). `tier` is the `tier` custom claim (one of
// viewer/developer/operator/admin/owner per
// docs/EPICS-1-6-unified-design.md §6.2). Both are case-insensitive.
//
// Conservative-by-default — any unrecognised shape returns "member",
// matching the policyModeCallerAuthorized fall-through. Upstream gates
// will 403 rather than silently grant a privilege the bearer doesn't
// hold.
func SMERoleFor(realmRoles []string, tier string) string {
	for _, r := range realmRoles {
		switch strings.ToLower(strings.TrimSpace(r)) {
		case "catalyst-owner":
			return "superadmin"
		case "catalyst-admin", "sovereign-admin", "application-admin":
			return "sovereign-admin"
		}
	}
	switch strings.ToLower(strings.TrimSpace(tier)) {
	case "owner":
		return "superadmin"
	case "admin":
		return "sovereign-admin"
	}
	return "member"
}
