package middleware

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

	"github.com/google/uuid"
)

// SessionResolver is the contract Auth depends on to translate a bearer
// token into an authenticated user ID. Defining the dependency as an
// interface here (rather than importing user.Service directly) keeps the
// transport layer free of any concrete user-package import, which is
// essential because the user module is implemented in a later phase and
// this middleware must be writeable and testable without it.
//
// Implementations must:
//   - return (uid, true, nil) on a valid session,
//   - return (uuid.Nil, false, nil) for a syntactically valid but unknown
//     or expired token (the middleware treats this the same as an error
//     for the client: 401),
//   - return a non-nil error for unexpected failures (e.g. database
//     outage); the middleware also returns 401 to avoid leaking which
//     branch failed.
type SessionResolver interface {
	ResolveSession(ctx context.Context, token string) (userID uuid.UUID, ok bool, err error)
}

// AuthOptions configures the Auth middleware. A zero value means
// "authentication required". Optional=true is intended for endpoints
// where an anonymous caller is allowed (e.g. a public landing page that
// personalises content when a user happens to be signed in).
//
// The struct exists rather than a bare bool so future extensions
// (RBAC scope checks, multi-realm support, etc.) can be added without
// breaking call sites.
type AuthOptions struct {
	// Optional, when true, lets requests without an Authorization header
	// pass through with no user_id in the context. Requests that DO carry
	// a token are still validated; an invalid token is rejected even when
	// Optional is set, so a forged token cannot bypass enforcement.
	Optional bool
}

// userIDKey is the context key used to attach the authenticated user ID
// to the request context. It deliberately reuses the ctxKey type defined
// in request_id.go (value 1) but with a distinct integer value (2) so
// keys cannot collide while still keeping the type unexported to the
// middleware package.
const userIDKey ctxKey = 2

// Auth returns HTTP middleware that enforces bearer token authentication
// using the supplied SessionResolver. On success the resolved user ID is
// stored in the request context and downstream handlers can retrieve it
// via UserIDFromContext.
//
// Failure modes all collapse to a 401 response with a small JSON body so
// the client cannot distinguish between "missing token", "malformed
// header", "unknown token" and "resolver failure" — this is intentional
// to limit the information available to attackers probing for valid
// tokens.
func Auth(resolver SessionResolver, opt AuthOptions) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			token := extractBearer(r)
			if token == "" {
				if opt.Optional {
					next.ServeHTTP(w, r)
					return
				}
				http.Error(w, `{"code":"unauthorized","message":"missing token"}`, http.StatusUnauthorized)
				return
			}
			uid, ok, err := resolver.ResolveSession(r.Context(), token)
			if err != nil || !ok {
				http.Error(w, `{"code":"unauthorized","message":"invalid token"}`, http.StatusUnauthorized)
				return
			}
			ctx := context.WithValue(r.Context(), userIDKey, uid)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

// UserIDFromContext returns the authenticated user ID stored in ctx by
// the Auth middleware. The boolean is false when no Auth middleware ran
// upstream or the request was admitted via AuthOptions.Optional with no
// token, so callers can distinguish "anonymous" from "logged-in user
// whose ID happens to be the zero UUID".
func UserIDFromContext(ctx context.Context) (uuid.UUID, bool) {
	v, ok := ctx.Value(userIDKey).(uuid.UUID)
	return v, ok
}

// extractBearer parses an Authorization header of the form "Bearer <tok>"
// and returns the trimmed token. It returns an empty string when the
// header is missing or does not start with the canonical "Bearer "
// prefix; case-insensitive matching is intentionally NOT performed
// because RFC 6750 specifies the literal "Bearer" scheme name and being
// strict here keeps the parsing surface small.
//
// For browser-native WebSocket connections, custom HTTP headers cannot be
// sent during the handshake. Two fallbacks are supported, in order of
// preference:
//  1. Sec-WebSocket-Protocol: the client sends a subprotocol entry
//     "acw-token.<tok>". The token is extracted from the header, which is
//     not logged by proxies/servers as readily as a URL query parameter.
//  2. ?token= query parameter (legacy): kept for backward compatibility.
func extractBearer(r *http.Request) string {
	h := r.Header.Get("Authorization")
	const prefix = "Bearer "
	if strings.HasPrefix(h, prefix) {
		return strings.TrimSpace(h[len(prefix):])
	}
	const wsTokenPrefix = "acw-token."
	for _, p := range strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ",") {
		p = strings.TrimSpace(p)
		if strings.HasPrefix(p, wsTokenPrefix) {
			return p[len(wsTokenPrefix):]
		}
	}
	if q := r.URL.Query().Get("token"); q != "" {
		return q
	}
	return ""
}
