// Package basicauth gates a route group behind HTTP Basic authentication.
//
// The middleware delegates the credential check to a Verifier.
// Production wires `*admin.Service` (Postgres-backed, bcrypt cost 12).
// The bundled StaticVerifier is a test utility — it accepts any
// non-empty username together with a fixed password so the contract
// test can exercise the admin route group without booting a database.
// Production wiring never references StaticVerifier.
package basicauth

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

	"galaxy/backend/internal/server/httperr"

	"github.com/gin-gonic/gin"
)

// DefaultRealm is the realm advertised on `WWW-Authenticate` for the admin
// surface.
const DefaultRealm = "galaxy-admin"

// usernameContextKey is the unexported context key used to expose the
// authenticated admin username to downstream handlers (e.g. for
// soft-delete audit trails). The unexported value type prevents
// accidental collisions with keys defined in unrelated packages.
type usernameContextKey struct{}

// Verifier validates a username/password pair. Implementations must run in
// constant time relative to the credential bytes.
type Verifier interface {
	// Verify reports whether the supplied credentials are accepted. A non-nil
	// error indicates an unexpected verifier failure, distinct from a clean
	// rejection (false, nil).
	Verify(ctx context.Context, username, password string) (bool, error)
}

// UsernameFromContext returns the authenticated admin username stored on
// ctx by Middleware. The boolean reports whether a value was found.
func UsernameFromContext(ctx context.Context) (string, bool) {
	if ctx == nil {
		return "", false
	}
	value, ok := ctx.Value(usernameContextKey{}).(string)
	if !ok {
		return "", false
	}
	return value, true
}

// WithUsername stores username on ctx under the package-private context
// key. Exposed for tests that need to build a context outside the
// middleware.
func WithUsername(ctx context.Context, username string) context.Context {
	return context.WithValue(ctx, usernameContextKey{}, username)
}

// Middleware returns a gin middleware that enforces Basic authentication via
// verifier. realm is advertised on `WWW-Authenticate`. A nil verifier behaves
// as a deny-all verifier, suitable for the operating mode where the admin
// surface must remain mounted but inaccessible.
func Middleware(verifier Verifier, realm string) gin.HandlerFunc {
	if realm == "" {
		realm = DefaultRealm
	}
	challenge := `Basic realm="` + realm + `"`

	return func(c *gin.Context) {
		username, password, ok := c.Request.BasicAuth()
		if !ok {
			c.Header("WWW-Authenticate", challenge)
			httperr.Abort(c, http.StatusUnauthorized, httperr.CodeUnauthorized, "basic authentication is required")
			return
		}

		if verifier == nil {
			c.Header("WWW-Authenticate", challenge)
			httperr.Abort(c, http.StatusUnauthorized, httperr.CodeUnauthorized, "credentials were rejected")
			return
		}

		accepted, err := verifier.Verify(c.Request.Context(), username, password)
		if err != nil {
			httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "credential verification failed")
			return
		}
		if !accepted {
			c.Header("WWW-Authenticate", challenge)
			httperr.Abort(c, http.StatusUnauthorized, httperr.CodeUnauthorized, "credentials were rejected")
			return
		}

		c.Request = c.Request.WithContext(WithUsername(c.Request.Context(), username))
		c.Next()
	}
}

// StaticVerifier accepts any non-empty username together with a
// fixed shared password. It is a test-only utility: the OpenAPI
// contract test wires it to exercise the admin route group without
// booting a database. Production wiring uses the Postgres-backed
// `*backend/internal/admin.Service`.
type StaticVerifier struct {
	// Password is the shared secret. An empty value disables the verifier
	// (every request is rejected).
	Password string
}

// NewStaticVerifier returns a StaticVerifier with the supplied password.
func NewStaticVerifier(password string) StaticVerifier {
	return StaticVerifier{Password: password}
}

// Verify accepts any non-empty username together with the configured password.
// The password comparison runs in constant time. An empty configured password
// rejects every request.
func (v StaticVerifier) Verify(_ context.Context, username, password string) (bool, error) {
	if strings.TrimSpace(username) == "" {
		return false, nil
	}
	if v.Password == "" {
		return false, nil
	}
	if subtle.ConstantTimeCompare([]byte(password), []byte(v.Password)) != 1 {
		return false, nil
	}
	return true, nil
}
