// Package authd — middleware.go: HTTP middleware for the JWT gate
// (M13a W2). Services like ingestd, routerd, deliverd-* use this
// to require a valid Bearer access token on protected routes, and
// to get the (user_id, tenant_id, role) claims into the request
// context for downstream logging and authorization.
//
// Threading: safe for concurrent use. The Authd it wraps is
// shared across requests.
package authd

import (
	"context"
	"errors"
	"net/http"
	"strings"
)

// ctxKey is unexported so external packages can't accidentally
// collide on the same context key.
type ctxKey int

const (
	ctxKeyClaims ctxKey = iota + 1
)

// ClaimsFromContext returns the JWT claims that RequireAuth stored
// in ctx, or nil if none. Used by handlers downstream of the
// middleware to read user_id / tenant_id / role.
func ClaimsFromContext(ctx context.Context) *AccessClaims {
	v, ok := ctx.Value(ctxKeyClaims).(*AccessClaims)
	if !ok {
		return nil
	}
	return v
}

// withClaims stores the claims in ctx. Internal — handlers should
// use ClaimsFromContext.
func withClaims(ctx context.Context, c *AccessClaims) context.Context {
	return context.WithValue(ctx, ctxKeyClaims, c)
}

// ---------------------------------------------------------------------------
// Middleware
// ---------------------------------------------------------------------------

// RequireAuth is the middleware. Wrap any handler that needs
// authentication:
//
//	mux.Handle("POST /v1/admin/foo", ad.RequireAuth(myHandler))
//
// On success, the handler is called with the claims in context
// (ClaimsFromContext). On failure, it writes a 401 with a JSON body
// and does NOT call the wrapped handler.
func (a *Authd) RequireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tok, err := bearerFromRequest(r)
		if err != nil {
			writeUnauthorized(w, "missing_bearer", err.Error())
			return
		}
		claims, err := a.VerifyAccessToken(tok)
		if err != nil {
			writeUnauthorized(w, "invalid_token", "access token invalid or expired")
			return
		}
		next.ServeHTTP(w, r.WithContext(withClaims(r.Context(), claims)))
	})
}

// RequireRole is like RequireAuth but additionally checks that the
// JWT carries one of the allowed roles. Returns 403 if the role
// doesn't match.
//
//	mux.Handle("POST /v1/users/invite",
//	    ad.RequireRole("super_admin", "tenant_admin")(inviteHandler))
func (a *Authd) RequireRole(allowed ...string) func(http.Handler) http.Handler {
	allowedSet := make(map[string]struct{}, len(allowed))
	for _, r := range allowed {
		allowedSet[r] = struct{}{}
	}
	return func(next http.Handler) http.Handler {
		return a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			claims := ClaimsFromContext(r.Context())
			if claims == nil {
				// Should not happen — RequireAuth would have 401'd.
				writeUnauthorized(w, "internal", "claims missing")
				return
			}
			if _, ok := allowedSet[claims.Role]; !ok {
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusForbidden)
				_, _ = w.Write([]byte(`{"error":"forbidden","message":"role not allowed"}`))
				return
			}
			next.ServeHTTP(w, r)
		}))
	}
}

// bearerFromRequest extracts the raw token from the Authorization
// header, accepting the case-insensitive "Bearer" scheme.
func bearerFromRequest(r *http.Request) (string, error) {
	h := r.Header.Get("Authorization")
	if h == "" {
		return "", errors.New("Authorization header missing")
	}
	// "Bearer <token>" — split on the first whitespace.
	const scheme = "bearer"
	if len(h) < len(scheme)+1 {
		return "", errors.New("Authorization header too short")
	}
	// case-insensitive scheme match
	prefix := strings.ToLower(h[:len(scheme)])
	if prefix != scheme {
		return "", errors.New("Authorization must use Bearer scheme")
	}
	rest := strings.TrimSpace(h[len(scheme):])
	if rest == "" {
		return "", errors.New("bearer token empty")
	}
	// Some clients send "Bearer\t<token>" — trim space above handles.
	// Also reject "Bearer  <token>" (double space) and "Bearer, ...".
	return rest, nil
}

// writeUnauthorized writes a 401 with a JSON error body. Duplicated
// in cmd/authd/main.go for now; v1.1 should move it to a shared
// helper in internal/httpserver.
func writeUnauthorized(w http.ResponseWriter, code, msg string) {
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("WWW-Authenticate", `Bearer realm="broad-announce"`)
	w.WriteHeader(http.StatusUnauthorized)
	_, _ = w.Write([]byte(`{"error":"` + code + `","message":"` + msg + `"}`))
}
