package userbilling

import (
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"database/sql"
	"embed"
	"encoding/base64"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"log"
	"math"
	"net/http"
	"os"
	"os/signal"
	"sort"
	"strconv"
	"strings"
	"syscall"
	"time"

	_ "github.com/jackc/pgx/v4/stdlib"
)

const serviceID = "hwlab-user-billing"

//go:embed migrations/*.sql
var migrationFS embed.FS

type Config struct {
	Addr                       string
	DatabaseURL                string
	RedisURL                   string
	InitialCredits             int64
	CreditPerThousandTok       int64
	SessionTTL                 time.Duration
	ReservationTTL             time.Duration
	RegistrationEnabled        bool
	InternalToken              string
	RequireInternalToken       bool
	MigrateOnStart             bool
	StateAuthority             string
	RuntimeNamespace           string
	ExternalDatabaseLabel      string
	BootstrapAdminID           string
	BootstrapAdminEmail        string
	BootstrapAdminUsername     string
	BootstrapAdminDisplayName  string
	BootstrapAdminPasswordHash string
	BootstrapAdminPassword     string
}

type Server struct {
	config Config
	db     *sql.DB
	mux    *http.ServeMux
}

type User struct {
	ID            string    `json:"id"`
	Email         string    `json:"email"`
	Username      string    `json:"username"`
	DisplayName   string    `json:"displayName"`
	Status        string    `json:"status"`
	Role          string    `json:"role"`
	EmailVerified bool      `json:"emailVerified"`
	CreatedAt     time.Time `json:"createdAt"`
}

type Principal struct {
	UserID   string   `json:"userId"`
	Email    string   `json:"email"`
	Username string   `json:"username"`
	Role     string   `json:"role"`
	Scopes   []string `json:"scopes"`
	AuthType string   `json:"authType"`
	KeyID    string   `json:"keyId,omitempty"`
}

type serviceUsageSummary struct {
	ServiceID    string    `json:"serviceId"`
	ResourceType string    `json:"resourceType"`
	Credits      int64     `json:"credits"`
	Quantity     int64     `json:"quantity"`
	RecordCount  int64     `json:"recordCount"`
	LastUsedAt   time.Time `json:"lastUsedAt"`
}

type ledgerSummaryRow struct {
	ID               string         `json:"id"`
	Kind             string         `json:"kind"`
	Reason           string         `json:"reason"`
	DeltaCredits     int64          `json:"deltaCredits"`
	BalanceBefore    *int64         `json:"balanceBefore,omitempty"`
	BalanceAfter     int64          `json:"balanceAfter"`
	Source           string         `json:"source"`
	Status           string         `json:"status"`
	IdempotencyKey   string         `json:"idempotencyKey,omitempty"`
	OperatorUserID   string         `json:"operatorUserId,omitempty"`
	OperatorUsername string         `json:"operatorUsername,omitempty"`
	Metadata         map[string]any `json:"metadata,omitempty"`
	CreatedAt        time.Time      `json:"createdAt"`
}

type adminUsageRow struct {
	ID             string         `json:"id"`
	ReservationID  string         `json:"reservationId,omitempty"`
	UserID         string         `json:"userId"`
	Email          string         `json:"email"`
	Username       string         `json:"username"`
	APIKeyID       string         `json:"apiKeyId,omitempty"`
	ServiceID      string         `json:"serviceId"`
	ResourceType   string         `json:"resourceType"`
	Unit           string         `json:"unit"`
	Quantity       int64          `json:"quantity"`
	Credits        int64          `json:"credits"`
	Status         string         `json:"status"`
	IdempotencyKey string         `json:"idempotencyKey,omitempty"`
	Metadata       map[string]any `json:"metadata,omitempty"`
	CreatedAt      time.Time      `json:"createdAt"`
}

type adminLedgerRow struct {
	ledgerSummaryRow
	UserID   string `json:"userId"`
	Email    string `json:"email"`
	Username string `json:"username"`
}

type adminAuditQuery struct {
	Page           int
	PageSize       int
	UserID         string
	Search         string
	ServiceID      string
	ResourceType   string
	APIKeyID       string
	Status         string
	Kind           string
	Source         string
	IdempotencyKey string
	From           string
	To             string
}

type creditAdjustResult struct {
	UserID         string `json:"userId"`
	BalanceBefore  int64  `json:"balanceBefore"`
	BalanceAfter   int64  `json:"balanceAfter"`
	DeltaCredits   int64  `json:"deltaCredits"`
	LedgerID       string `json:"ledgerId"`
	Kind           string `json:"kind"`
	Reason         string `json:"reason"`
	Source         string `json:"source"`
	Status         string `json:"status"`
	IdempotencyKey string `json:"idempotencyKey,omitempty"`
	Idempotent     bool   `json:"idempotent"`
}

type reservationSummaryRow struct {
	ReservationID    string    `json:"reservationId"`
	ServiceID        string    `json:"serviceId"`
	ResourceType     string    `json:"resourceType"`
	EstimatedCredits int64     `json:"estimatedCredits"`
	EstimatedTokens  int64     `json:"estimatedTokens"`
	Status           string    `json:"status"`
	ExpiresAt        time.Time `json:"expiresAt"`
	CreatedAt        time.Time `json:"createdAt"`
}

type billingPlanSummary struct {
	ID          string         `json:"id"`
	DisplayName string         `json:"displayName"`
	Status      string         `json:"status"`
	Description string         `json:"description"`
	Metadata    map[string]any `json:"metadata,omitempty"`
}

type resourceEntitlementSummary struct {
	ID                        string         `json:"id,omitempty"`
	PlanID                    string         `json:"planId,omitempty"`
	ResourceType              string         `json:"resourceType"`
	Enabled                   bool           `json:"enabled"`
	MonthlyQuotaCredits       int64          `json:"monthlyQuotaCredits"`
	MonthlyQuotaUnlimited     bool           `json:"monthlyQuotaUnlimited"`
	MonthlyUsageCredits       int64          `json:"monthlyUsageCredits"`
	MonthlyRemainingCredits   int64          `json:"monthlyRemainingCredits"`
	MaxConcurrentReservations int64          `json:"maxConcurrentReservations"`
	ConcurrencyUnlimited      bool           `json:"concurrencyUnlimited"`
	ActiveReservations        int64          `json:"activeReservations"`
	RPMLimit                  int64          `json:"rpmLimit"`
	RPMUnlimited              bool           `json:"rpmUnlimited"`
	RPMUsed                   int64          `json:"rpmUsed"`
	LimitSemantics            string         `json:"limitSemantics"`
	Metadata                  map[string]any `json:"metadata,omitempty"`
}

type adminBillingUserSummary struct {
	User         User                         `json:"user"`
	PlanID       string                       `json:"planId"`
	Plan         billingPlanSummary           `json:"plan,omitempty"`
	Entitlements []resourceEntitlementSummary `json:"entitlements,omitempty"`
	Credits      creditSummary                `json:"credits"`
	Usage        userUsageAggregate           `json:"usage"`
	APIKeys      apiKeyAggregate              `json:"apiKeys"`
	Reservations reservationAdminSummary      `json:"reservations"`
}

type creditSummary struct {
	Balance   int64 `json:"balance"`
	Reserved  int64 `json:"reserved"`
	Available int64 `json:"available"`
}

type userUsageAggregate struct {
	TotalCredits  int64      `json:"totalCredits"`
	TotalQuantity int64      `json:"totalQuantity"`
	RecordCount   int64      `json:"recordCount"`
	LastUsedAt    *time.Time `json:"lastUsedAt"`
}

type apiKeyAggregate struct {
	ActiveCount int64      `json:"activeCount"`
	TotalCount  int64      `json:"totalCount"`
	LastUsedAt  *time.Time `json:"lastUsedAt"`
}

type apiKeySummary struct {
	ID         string     `json:"id"`
	Name       string     `json:"name"`
	Prefix     string     `json:"prefix"`
	Scopes     []string   `json:"scopes"`
	Status     string     `json:"status"`
	CreatedAt  time.Time  `json:"createdAt"`
	LastUsedAt *time.Time `json:"lastUsedAt"`
	RevokedAt  *time.Time `json:"revokedAt"`
}

type adminUsersQuery struct {
	Page     int
	PageSize int
	Search   string
	Status   string
	Role     string
}

type adminUserDetail struct {
	Summary      adminBillingUserSummary `json:"summary"`
	Ledger       []ledgerSummaryRow      `json:"ledger"`
	APIKeys      []apiKeySummary         `json:"apiKeys"`
	UsageBy      []serviceUsageSummary   `json:"usageByService"`
	Reservations []reservationSummaryRow `json:"reservations"`
}

type billingPlanDetail struct {
	Plan         billingPlanSummary           `json:"plan"`
	Entitlements []resourceEntitlementSummary `json:"entitlements"`
}

type reservationAdminSummary struct {
	ActiveCount int64                   `json:"activeCount"`
	Active      []reservationSummaryRow `json:"active"`
}

type apiError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func Run(parent context.Context) error {
	cfg := LoadConfig(os.Environ())
	server, err := NewServer(parent, cfg)
	if err != nil {
		return err
	}
	defer server.Close()

	ctx, stop := signal.NotifyContext(parent, syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	httpServer := &http.Server{
		Addr:              cfg.Addr,
		Handler:           server,
		ReadHeaderTimeout: 10 * time.Second,
	}

	errCh := make(chan error, 1)
	go func() {
		log.Printf("%s listening on %s", serviceID, cfg.Addr)
		errCh <- httpServer.ListenAndServe()
	}()

	select {
	case <-ctx.Done():
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		_ = httpServer.Shutdown(shutdownCtx)
		return nil
	case err := <-errCh:
		if errors.Is(err, http.ErrServerClosed) {
			return nil
		}
		return err
	}
}

func LoadConfig(env []string) Config {
	values := map[string]string{}
	for _, item := range env {
		name, value, ok := strings.Cut(item, "=")
		if ok {
			values[name] = value
		}
	}
	port := first(values["HWLAB_USER_BILLING_PORT"], values["PORT"], "6670")
	cfg := Config{
		Addr:                       first(values["HWLAB_USER_BILLING_ADDR"], ":"+port),
		DatabaseURL:                first(values["HWLAB_USER_BILLING_DB_URL"], values["DATABASE_URL"]),
		RedisURL:                   values["HWLAB_USER_BILLING_REDIS_URL"],
		InitialCredits:             parseInt64(values["HWLAB_USER_BILLING_INITIAL_CREDITS"], 0),
		CreditPerThousandTok:       parseInt64(values["HWLAB_USER_BILLING_CREDIT_PER_1K_TOKENS"], 1),
		SessionTTL:                 parseDurationHours(values["HWLAB_USER_BILLING_SESSION_TTL_HOURS"], 720),
		ReservationTTL:             parseDurationMinutes(values["HWLAB_USER_BILLING_RESERVATION_TTL_MINUTES"], 30),
		RegistrationEnabled:        values["HWLAB_USER_BILLING_REGISTRATION_ENABLED"] != "0",
		InternalToken:              values["HWLAB_USER_BILLING_INTERNAL_TOKEN"],
		RequireInternalToken:       values["HWLAB_USER_BILLING_REQUIRE_INTERNAL_TOKEN"] == "1",
		MigrateOnStart:             values["HWLAB_USER_BILLING_MIGRATE_ON_START"] != "0",
		StateAuthority:             first(values["HWLAB_USER_BILLING_STATE_AUTHORITY"], "pk01-postgres"),
		RuntimeNamespace:           first(values["POD_NAMESPACE"], values["HWLAB_NAMESPACE"], "hwlab-v03"),
		ExternalDatabaseLabel:      first(values["HWLAB_USER_BILLING_EXTERNAL_DB_LABEL"], "pk01-external-postgres"),
		BootstrapAdminID:           first(values["HWLAB_BOOTSTRAP_ADMIN_ID"], "usr_bootstrap_admin"),
		BootstrapAdminEmail:        values["HWLAB_BOOTSTRAP_ADMIN_EMAIL"],
		BootstrapAdminUsername:     first(values["HWLAB_BOOTSTRAP_ADMIN_USERNAME"], "admin"),
		BootstrapAdminDisplayName:  first(values["HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME"], "HWLAB Admin"),
		BootstrapAdminPasswordHash: values["HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH"],
		BootstrapAdminPassword:     values["HWLAB_BOOTSTRAP_ADMIN_PASSWORD"],
	}
	return cfg
}

func NewServer(ctx context.Context, cfg Config) (*Server, error) {
	server := &Server{config: cfg, mux: http.NewServeMux()}
	if cfg.DatabaseURL != "" {
		db, err := sql.Open("pgx", cfg.DatabaseURL)
		if err != nil {
			return nil, err
		}
		db.SetMaxOpenConns(envInt("HWLAB_USER_BILLING_DB_POOL_MAX", 4))
		db.SetMaxIdleConns(4)
		db.SetConnMaxLifetime(30 * time.Minute)
		server.db = db
		if cfg.MigrateOnStart {
			migrateCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
			defer cancel()
			if err := server.migrate(migrateCtx); err != nil {
				return nil, fmt.Errorf("migrate user billing schema: %w", err)
			}
		}
		bootstrapCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
		defer cancel()
		if err := server.ensureBootstrapAdmin(bootstrapCtx); err != nil {
			return nil, fmt.Errorf("ensure bootstrap admin: %w", err)
		}
	}
	server.routes()
	return server, nil
}

func (s *Server) ensureBootstrapAdmin(ctx context.Context) error {
	if s.db == nil {
		return nil
	}
	username := normalizeUsername(s.config.BootstrapAdminUsername)
	if username == "" {
		return nil
	}
	passwordHash := strings.TrimSpace(s.config.BootstrapAdminPasswordHash)
	if passwordHash == "" && strings.TrimSpace(s.config.BootstrapAdminPassword) != "" {
		hash, err := hashPassword(s.config.BootstrapAdminPassword)
		if err != nil {
			return err
		}
		passwordHash = hash
	}
	if passwordHash == "" {
		return nil
	}
	adminID := strings.TrimSpace(first(s.config.BootstrapAdminID, "usr_bootstrap_admin"))
	email := strings.ToLower(strings.TrimSpace(s.config.BootstrapAdminEmail))
	if email == "" {
		email = username + "@bootstrap.hwlab.local"
	}
	displayName := strings.TrimSpace(first(s.config.BootstrapAdminDisplayName, "HWLAB Admin"))
	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return err
	}
	defer tx.Rollback()
	result, err := tx.ExecContext(ctx, `UPDATE hwlab_users SET email = $3, display_name = $4, password_hash = $5, status = 'active', role = 'admin', email_verified = true, updated_at = now() WHERE id = $1 OR username = $2`, adminID, username, email, displayName, passwordHash)
	if err != nil {
		return err
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return err
	}
	if rows == 0 {
		_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_users (id, email, username, display_name, password_hash, status, role, email_verified) VALUES ($1, $2, $3, $4, $5, 'active', 'admin', true)`, adminID, email, username, displayName, passwordHash)
		if err != nil {
			return err
		}
	}
	_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_credit_accounts (user_id, balance_credits, reserved_credits, plan_id) VALUES ($1, $2, 0, 'default') ON CONFLICT (user_id) DO NOTHING`, adminID, nonNegative(s.config.InitialCredits))
	if err != nil {
		return err
	}
	return tx.Commit()
}

func (s *Server) Close() {
	if s.db != nil {
		_ = s.db.Close()
	}
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s.mux.ServeHTTP(w, r)
}

func (s *Server) routes() {
	s.route(http.MethodGet, "/health/live", s.handleLive)
	s.route(http.MethodGet, "/health/ready", s.handleReady)
	s.route(http.MethodPost, "/v1/auth/register", s.handleRegister)
	s.route(http.MethodPost, "/v1/auth/login", s.handleLogin)
	s.route(http.MethodPost, "/v1/auth/logout", s.handleLogout)
	s.route(http.MethodPost, "/v1/auth/refresh", s.handleRefresh)
	s.route(http.MethodGet, "/v1/me", s.handleMe)
	s.route(http.MethodPatch, "/v1/me/profile", s.handleUpdateProfile)
	s.route(http.MethodPost, "/v1/me/password", s.handleChangePassword)
	s.route(http.MethodGet, "/v1/billing/summary", s.handleBillingSummary)
	s.route(http.MethodPost, "/v1/redeem", s.handleRedeem)
	s.route(http.MethodGet, "/v1/subscription/summary", s.handleSubscriptionSummary)
	s.route(http.MethodGet, "/v1/payments/summary", s.handlePaymentSummary)
	s.mux.HandleFunc("/v1/api-keys", s.handleAPIKeys)
	s.mux.HandleFunc("/v1/api-keys/", s.handleAPIKeyByID)
	s.route(http.MethodPost, "/internal/auth/introspect", s.handleIntrospect)
	s.route(http.MethodPost, "/internal/billing/preflight", s.handleBillingPreflight)
	s.route(http.MethodPost, "/internal/billing/record", s.handleBillingRecord)
	s.route(http.MethodPost, "/internal/billing/release", s.handleBillingRelease)
	s.route(http.MethodGet, "/internal/admin/billing/summary", s.handleAdminBillingSummary)
	s.route(http.MethodGet, "/internal/admin/billing/plans", s.handleAdminBillingPlans)
	s.route(http.MethodGet, "/internal/admin/usage", s.handleAdminUsage)
	s.route(http.MethodGet, "/internal/admin/usage/export", s.handleAdminUsageExport)
	s.route(http.MethodGet, "/internal/admin/ledger", s.handleAdminLedger)
	s.route(http.MethodGet, "/internal/admin/ledger/export", s.handleAdminLedgerExport)
	s.mux.HandleFunc("/internal/admin/redeem-codes", s.handleAdminRedeemCodes)
	s.mux.HandleFunc("/internal/admin/redeem-codes/", s.handleAdminRedeemCodeByID)
	s.route(http.MethodGet, "/internal/admin/redeem-redemptions", s.handleAdminRedeemRedemptions)
	s.route(http.MethodPost, "/internal/admin/credits/adjust", s.handleAdminCreditAdjust)
	s.mux.HandleFunc("/internal/admin/users", s.handleAdminUsers)
	s.mux.HandleFunc("/internal/admin/users/", s.handleAdminUserByID)
	s.route(http.MethodPost, "/internal/admin/users/status", s.handleAdminUserStatus)
}

func (s *Server) route(method, path string, handler http.HandlerFunc) {
	s.mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
		if r.Method != method {
			w.Header().Set("Allow", method)
			writeAPIError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
			return
		}
		handler(w, r)
	})
}

func (s *Server) migrate(ctx context.Context) error {
	if s.db == nil {
		return nil
	}
	files, err := fs.Glob(migrationFS, "migrations/*.sql")
	if err != nil {
		return err
	}
	sort.Strings(files)
	for _, name := range files {
		body, err := migrationFS.ReadFile(name)
		if err != nil {
			return err
		}
		for _, statement := range splitSQLStatements(string(body)) {
			if _, err := s.db.ExecContext(ctx, statement); err != nil {
				return fmt.Errorf("%s: %w", name, err)
			}
		}
	}
	return nil
}

func splitSQLStatements(text string) []string {
	parts := strings.Split(text, ";\n")
	statements := make([]string, 0, len(parts))
	for _, part := range parts {
		stmt := strings.TrimSpace(part)
		if stmt == "" {
			continue
		}
		if !strings.HasSuffix(stmt, ";") {
			stmt += ";"
		}
		statements = append(statements, stmt)
	}
	return statements
}

func (s *Server) handleLive(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, map[string]any{
		"serviceId":             serviceID,
		"status":                "ok",
		"stateless":             true,
		"stateAuthority":        s.config.StateAuthority,
		"database":              map[string]any{"configured": s.db != nil, "authority": s.config.ExternalDatabaseLabel},
		"redis":                 map[string]any{"configured": s.config.RedisURL != "", "role": "cache-only"},
		"namespace":             s.config.RuntimeNamespace,
		"registrationEnabled":   s.config.RegistrationEnabled,
		"internalTokenRequired": s.config.RequireInternalToken,
	})
}

func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
	if s.db == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]any{"serviceId": serviceID, "status": "degraded", "reason": "database-url-not-configured"})
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
	defer cancel()
	if err := s.db.PingContext(ctx); err != nil {
		s.logDatabaseError("ready_ping", err)
		writeJSON(w, http.StatusServiceUnavailable, map[string]any{"serviceId": serviceID, "status": "degraded", "reason": "database-ping-failed", "database": map[string]any{"configured": true, "authority": s.config.ExternalDatabaseLabel, "errorClass": classifyDatabaseError(err)}})
		return
	}
	if err := s.readyWriteProbe(ctx); err != nil {
		s.logDatabaseError("ready_write_probe", err)
		writeJSON(w, http.StatusServiceUnavailable, map[string]any{"serviceId": serviceID, "status": "degraded", "reason": "database-transaction-probe-failed", "database": map[string]any{"configured": true, "authority": s.config.ExternalDatabaseLabel, "errorClass": classifyDatabaseError(err)}})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"serviceId": serviceID, "status": "ready", "database": map[string]any{"configured": true, "authority": s.config.ExternalDatabaseLabel, "transactionProbe": "write-rollback-ok"}})
}

func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
	if !s.registrationAvailable(w) {
		return
	}
	var req struct {
		Email       string `json:"email"`
		Username    string `json:"username"`
		DisplayName string `json:"displayName"`
		Password    string `json:"password"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	email := strings.ToLower(strings.TrimSpace(req.Email))
	username := normalizeUsername(req.Username)
	if email == "" || !strings.Contains(email, "@") || username == "" || len(req.Password) < 10 {
		writeAPIError(w, http.StatusBadRequest, "invalid_registration", "email, username and a password with at least 10 characters are required")
		return
	}
	passwordHash, err := hashPassword(req.Password)
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "password_hash_failed", "could not hash password")
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		s.logDatabaseError("register_begin_tx", err)
		writeAPIError(w, http.StatusInternalServerError, "transaction_failed", "could not start transaction")
		return
	}
	defer tx.Rollback()
	user := User{ID: newID("usr"), Email: email, Username: username, DisplayName: strings.TrimSpace(req.DisplayName), Status: "active", Role: "user"}
	err = tx.QueryRowContext(ctx, `INSERT INTO hwlab_users (id, email, username, display_name, password_hash, status, role) VALUES ($1, $2, $3, $4, $5, 'active', 'user') RETURNING created_at`, user.ID, user.Email, user.Username, user.DisplayName, passwordHash).Scan(&user.CreatedAt)
	if err != nil {
		if isDuplicate(err) {
			writeAPIError(w, http.StatusConflict, "user_exists", "email or username already exists")
			return
		}
		writeAPIError(w, http.StatusInternalServerError, "user_create_failed", "could not create user")
		return
	}
	_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_credit_accounts (user_id, balance_credits, reserved_credits, plan_id) VALUES ($1, $2, 0, 'default')`, user.ID, s.config.InitialCredits)
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "credit_account_failed", "could not create credit account")
		return
	}
	if s.config.InitialCredits > 0 {
		_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_credit_ledger (id, user_id, delta_credits, balance_before, balance_after, kind, reason, source, status, metadata) VALUES ($1, $2, $3, 0, $3, 'grant', 'registration_initial_credit', 'registration', 'applied', '{}'::jsonb)`, newID("led"), user.ID, s.config.InitialCredits)
		if err != nil {
			writeAPIError(w, http.StatusInternalServerError, "credit_ledger_failed", "could not write initial credit ledger")
			return
		}
	}
	if err := tx.Commit(); err != nil {
		writeAPIError(w, http.StatusInternalServerError, "transaction_commit_failed", "could not commit registration")
		return
	}
	writeJSON(w, http.StatusCreated, map[string]any{"user": user, "initialCredits": s.config.InitialCredits})
}

func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
	trace := newOtelTraceContext(r.Header.Get("traceparent"))
	if traceparent := trace.traceparent(); traceparent != "" {
		w.Header().Set("traceparent", traceparent)
		w.Header().Set("x-hwlab-otel-trace-id", trace.TraceID)
	}
	startedAt := time.Now().UTC()
	status := http.StatusInternalServerError
	outcome := "error"
	errorCode := ""
	defer func() {
		attrs := map[string]any{
			"http.method":                  http.MethodPost,
			"http.route":                   "/v1/auth/login",
			"http.status_code":             status,
			"auth.outcome":                 outcome,
			"user_billing.service_id":      serviceID,
			"user_billing.values_redacted": true,
		}
		s.emitOtelSpanAsync("user-billing.auth.login", trace, trace.ServerSpanID, trace.ParentSpanID, otelSpanKindServer, startedAt, attrs, status, errorCode)
	}()
	if !s.databaseAvailable(w) {
		status = http.StatusServiceUnavailable
		outcome = "state_backend_not_configured"
		errorCode = "state_backend_not_configured"
		return
	}
	var req struct {
		Email    string `json:"email"`
		Username string `json:"username"`
		Password string `json:"password"`
	}
	if !decodeJSON(w, r, &req) {
		status = http.StatusBadRequest
		outcome = "invalid_json"
		errorCode = "invalid_json"
		return
	}
	login := strings.ToLower(strings.TrimSpace(first(req.Email, req.Username)))
	if login == "" || req.Password == "" {
		status = http.StatusBadRequest
		outcome = "invalid_login"
		errorCode = "invalid_login"
		writeAPIError(w, http.StatusBadRequest, "invalid_login", "email or username and password are required")
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	var user User
	var passwordHash string
	dbStartedAt := time.Now().UTC()
	err := s.db.QueryRowContext(ctx, `SELECT id, email, username, display_name, password_hash, status, role, email_verified, created_at FROM hwlab_users WHERE lower(email) = $1 OR lower(username) = $1`, login).Scan(&user.ID, &user.Email, &user.Username, &user.DisplayName, &passwordHash, &user.Status, &user.Role, &user.EmailVerified, &user.CreatedAt)
	dbErrorCode := ""
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		dbErrorCode = first(sqlErrorCode(err), "db_query_failed")
	}
	s.emitOtelSpanAsync("user-billing.postgres.login_user", trace, newOtelSpanID(), trace.ServerSpanID, otelSpanKindInternal, dbStartedAt, map[string]any{
		"db.system":                    "postgresql",
		"db.operation":                 "SELECT",
		"db.sql.table":                 "hwlab_users",
		"db.rows_found":                err == nil,
		"user_billing.values_redacted": true,
		"db.error_code":                dbErrorCode,
	}, 0, dbErrorCode)
	if errors.Is(err, sql.ErrNoRows) {
		status = http.StatusUnauthorized
		outcome = "invalid_credentials"
		errorCode = "invalid_credentials"
		writeAPIError(w, http.StatusUnauthorized, "invalid_credentials", "invalid email, username or password")
		return
	}
	if err != nil {
		status = http.StatusInternalServerError
		outcome = "db_error"
		errorCode = first(dbErrorCode, "login_failed")
		writeAPIError(w, http.StatusInternalServerError, errorCode, "could not load user")
		return
	}
	if !verifyPassword(passwordHash, req.Password) {
		status = http.StatusUnauthorized
		outcome = "invalid_credentials"
		errorCode = "invalid_credentials"
		writeAPIError(w, http.StatusUnauthorized, "invalid_credentials", "invalid email, username or password")
		return
	}
	if user.Status != "active" {
		status = http.StatusForbidden
		outcome = "user_disabled"
		errorCode = "user_disabled"
		writeAPIError(w, http.StatusForbidden, "user_disabled", "user is not active")
		return
	}
	sessionToken := "hws_" + randomToken(32)
	expiresAt := time.Now().UTC().Add(s.config.SessionTTL)
	_, err = s.db.ExecContext(ctx, `INSERT INTO hwlab_user_sessions (id, user_id, token_hash, expires_at) VALUES ($1, $2, $3, $4)`, newID("ses"), user.ID, tokenHash(sessionToken), expiresAt)
	if err != nil {
		status = http.StatusInternalServerError
		outcome = "session_create_failed"
		errorCode = first(sqlErrorCode(err), "session_create_failed")
		writeAPIError(w, http.StatusInternalServerError, errorCode, "could not create session")
		return
	}
	status = http.StatusOK
	outcome = "session_created"
	writeJSON(w, http.StatusOK, map[string]any{"token": sessionToken, "tokenType": "Bearer", "expiresAt": expiresAt, "user": user})
}

func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
	if !s.databaseAvailable(w) {
		return
	}
	token := bearerToken(r)
	if token == "" {
		writeAPIError(w, http.StatusUnauthorized, "missing_bearer", "authorization bearer token is required")
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
	defer cancel()
	_, _ = s.db.ExecContext(ctx, `UPDATE hwlab_user_sessions SET revoked_at = now() WHERE token_hash = $1 AND revoked_at IS NULL`, tokenHash(token))
	writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}

func (s *Server) handleRefresh(w http.ResponseWriter, r *http.Request) {
	principal, ok := s.requirePrincipal(w, r)
	if !ok {
		return
	}
	if principal.AuthType != "session" {
		writeAPIError(w, http.StatusForbidden, "session_required", "only session tokens can be refreshed")
		return
	}
	expiresAt := time.Now().UTC().Add(s.config.SessionTTL)
	ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
	defer cancel()
	_, err := s.db.ExecContext(ctx, `UPDATE hwlab_user_sessions SET expires_at = $1 WHERE token_hash = $2 AND revoked_at IS NULL`, expiresAt, tokenHash(bearerToken(r)))
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "session_refresh_failed", "could not refresh session")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"expiresAt": expiresAt})
}

func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) {
	principal, ok := s.requirePrincipal(w, r)
	if !ok {
		return
	}
	balance, reserved, _ := s.accountBalance(r.Context(), principal.UserID)
	writeJSON(w, http.StatusOK, map[string]any{"principal": principal, "credits": map[string]any{"balance": balance, "reserved": reserved, "available": balance - reserved}})
}

func (s *Server) handleUpdateProfile(w http.ResponseWriter, r *http.Request) {
	principal, ok := s.requirePrincipal(w, r)
	if !ok {
		return
	}
	var req struct {
		Username    *string `json:"username"`
		DisplayName *string `json:"displayName"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	updates := []string{}
	args := []any{principal.UserID}
	if req.Username != nil {
		username := normalizeUsername(*req.Username)
		if strings.TrimSpace(*req.Username) != "" && username == "" {
			writeAPIError(w, http.StatusBadRequest, "invalid_username", "username must be 3-64 characters and contain only lowercase letters, digits, '-' or '_'")
			return
		}
		if username != "" && username != principal.Username {
			args = append(args, username)
			updates = append(updates, "username = $"+strconv.Itoa(len(args)))
		}
	}
	if req.DisplayName != nil {
		args = append(args, strings.TrimSpace(*req.DisplayName))
		updates = append(updates, "display_name = $"+strconv.Itoa(len(args)))
	}
	if len(updates) == 0 {
		writeAPIError(w, http.StatusBadRequest, "no_profile_fields", "at least one profile field must be provided")
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	var user User
	query := `UPDATE hwlab_users SET ` + strings.Join(updates, ", ") + `, updated_at = now() WHERE id = $1 RETURNING id, email, username, display_name, status, role, email_verified, created_at`
	err := s.db.QueryRowContext(ctx, query, args...).Scan(&user.ID, &user.Email, &user.Username, &user.DisplayName, &user.Status, &user.Role, &user.EmailVerified, &user.CreatedAt)
	if err != nil {
		if isDuplicate(err) {
			writeAPIError(w, http.StatusConflict, "username_exists", "username already exists")
			return
		}
		writeAPIError(w, http.StatusInternalServerError, "profile_update_failed", "could not update profile")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-profile-v1", "updated": true, "user": user, "valuesRedacted": true})
}

func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) {
	principal, ok := s.requirePrincipal(w, r)
	if !ok {
		return
	}
	if principal.AuthType != "session" {
		writeAPIError(w, http.StatusForbidden, "session_required", "only session-authenticated users can change password")
		return
	}
	var req struct {
		CurrentPassword string `json:"currentPassword"`
		Password        string `json:"password"`
		NewPassword     string `json:"newPassword"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	newPassword := first(req.NewPassword, req.Password)
	if req.CurrentPassword == "" || len(newPassword) < 10 {
		writeAPIError(w, http.StatusBadRequest, "invalid_password_change", "currentPassword and a new password with at least 10 characters are required")
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	var currentHash string
	if err := s.db.QueryRowContext(ctx, `SELECT password_hash FROM hwlab_users WHERE id = $1 AND status = 'active'`, principal.UserID).Scan(&currentHash); err != nil {
		writeAPIError(w, http.StatusUnauthorized, "invalid_subject", "active user was not found")
		return
	}
	if !verifyPassword(currentHash, req.CurrentPassword) {
		writeAPIError(w, http.StatusUnauthorized, "invalid_current_password", "current password is invalid")
		return
	}
	passwordHash, err := hashPassword(newPassword)
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "password_hash_failed", "could not hash password")
		return
	}
	if _, err := s.db.ExecContext(ctx, `UPDATE hwlab_users SET password_hash = $2, updated_at = now() WHERE id = $1`, principal.UserID, passwordHash); err != nil {
		writeAPIError(w, http.StatusInternalServerError, "password_update_failed", "could not update password")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-profile-v1", "changed": true, "valuesRedacted": true})
}

func (s *Server) handleBillingSummary(w http.ResponseWriter, r *http.Request) {
	principal, ok := s.requirePrincipal(w, r)
	if !ok {
		return
	}
	limit := queryLimit(r, "limit", 20, 100)
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()

	balance, reserved, err := s.accountBalance(ctx, principal.UserID)
	if err != nil {
		s.logDatabaseError("billing_summary_account", err)
		writeAPIError(w, http.StatusInternalServerError, "billing_summary_failed", "could not load credit account")
		return
	}
	totalCredits, totalQuantity, recordCount, err := s.usageTotals(ctx, principal.UserID)
	if err != nil {
		s.logDatabaseError("billing_summary_usage_totals", err)
		writeAPIError(w, http.StatusInternalServerError, "billing_summary_failed", "could not load usage totals")
		return
	}
	byService, err := s.usageByService(ctx, principal.UserID, 20)
	if err != nil {
		s.logDatabaseError("billing_summary_usage_by_service", err)
		writeAPIError(w, http.StatusInternalServerError, "billing_summary_failed", "could not load service usage")
		return
	}
	ledgerRows, err := s.ledgerRows(ctx, principal.UserID, limit)
	if err != nil {
		s.logDatabaseError("billing_summary_ledger", err)
		writeAPIError(w, http.StatusInternalServerError, "billing_summary_failed", "could not load credit ledger")
		return
	}
	reservations, err := s.activeReservations(ctx, principal.UserID, 20)
	if err != nil {
		s.logDatabaseError("billing_summary_reservations", err)
		writeAPIError(w, http.StatusInternalServerError, "billing_summary_failed", "could not load active reservations")
		return
	}
	planID, plan, entitlements, err := s.accountPlanContext(ctx, principal.UserID)
	if err != nil {
		s.logDatabaseError("billing_summary_entitlements", err)
		writeAPIError(w, http.StatusInternalServerError, "billing_summary_failed", "could not load billing plan entitlements")
		return
	}

	writeJSON(w, http.StatusOK, map[string]any{
		"contractVersion": "user-billing-summary-v1",
		"serviceId":       serviceID,
		"status":          "ok",
		"principal":       principal,
		"credits": map[string]any{
			"balance":   balance,
			"reserved":  reserved,
			"available": balance - reserved,
		},
		"planId":       planID,
		"plan":         plan,
		"entitlements": entitlements,
		"usage": map[string]any{
			"totalCredits":  totalCredits,
			"totalQuantity": totalQuantity,
			"recordCount":   recordCount,
			"byService":     byService,
		},
		"ledger": map[string]any{
			"count": len(ledgerRows),
			"limit": limit,
			"rows":  ledgerRows,
		},
		"reservations": map[string]any{
			"activeCount": len(reservations),
			"active":      reservations,
		},
		"state": map[string]any{
			"stateless":      true,
			"stateAuthority": s.config.StateAuthority,
			"database":       map[string]any{"authority": s.config.ExternalDatabaseLabel},
			"redis":          map[string]any{"role": "cache-only"},
		},
		"valuesRedacted": true,
	})
}

func (s *Server) handleAPIKeys(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		s.handleListAPIKeys(w, r)
	case http.MethodPost:
		s.handleCreateAPIKey(w, r)
	default:
		w.Header().Set("Allow", "GET, POST")
		writeAPIError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
	}
}

func (s *Server) handleCreateAPIKey(w http.ResponseWriter, r *http.Request) {
	principal, ok := s.requirePrincipal(w, r)
	if !ok {
		return
	}
	var req struct {
		Name   string   `json:"name"`
		Scopes []string `json:"scopes"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	name := strings.TrimSpace(first(req.Name, "default"))
	scopes := req.Scopes
	if len(scopes) == 0 {
		scopes = []string{"api"}
	}
	scopesJSON, _ := json.Marshal(scopes)
	key := "hwl_" + randomToken(32)
	keyID := newID("key")
	prefix := key[:min(len(key), 16)]
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	_, err := s.db.ExecContext(ctx, `INSERT INTO hwlab_api_keys (id, user_id, name, key_prefix, key_hash, scopes_json) VALUES ($1, $2, $3, $4, $5, $6::jsonb)`, keyID, principal.UserID, name, prefix, tokenHash(key), string(scopesJSON))
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "api_key_create_failed", "could not create API key")
		return
	}
	summary := apiKeySummary{ID: keyID, Name: name, Prefix: prefix, Scopes: scopes, Status: "active", CreatedAt: time.Now().UTC()}
	writeJSON(w, http.StatusCreated, map[string]any{"contractVersion": "user-api-key-v1", "id": keyID, "key": key, "secret": key, "apiKey": summary, "prefix": prefix, "scopes": scopes, "created": true, "valuesRedacted": true})
}

func (s *Server) handleListAPIKeys(w http.ResponseWriter, r *http.Request) {
	principal, ok := s.requirePrincipal(w, r)
	if !ok {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	keys, err := s.apiKeysForUser(ctx, principal.UserID)
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "api_key_list_failed", "could not list API keys")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-api-key-v1", "keys": keys, "count": len(keys), "valuesRedacted": true})
}

func (s *Server) handleAPIKeyByID(w http.ResponseWriter, r *http.Request) {
	principal, ok := s.requirePrincipal(w, r)
	if !ok {
		return
	}
	keyID := strings.TrimPrefix(r.URL.Path, "/v1/api-keys/")
	keyID = strings.TrimSpace(keyID)
	if keyID == "" || strings.Contains(keyID, "/") {
		writeAPIError(w, http.StatusNotFound, "api_key_not_found", "API key was not found")
		return
	}
	switch r.Method {
	case http.MethodPatch:
		s.handleUpdateAPIKey(w, r, principal.UserID, keyID)
	case http.MethodDelete:
		s.handleRevokeAPIKey(w, r, principal.UserID, keyID)
	default:
		w.Header().Set("Allow", "PATCH, DELETE")
		writeAPIError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
	}
}

func (s *Server) handleUpdateAPIKey(w http.ResponseWriter, r *http.Request, userID, keyID string) {
	var req struct {
		Name   string   `json:"name"`
		Scopes []string `json:"scopes"`
		Status string   `json:"status"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	updates := []string{}
	args := []any{keyID, userID}
	if strings.TrimSpace(req.Name) != "" {
		args = append(args, strings.TrimSpace(req.Name))
		updates = append(updates, "name = $"+strconv.Itoa(len(args)))
	}
	if len(req.Scopes) > 0 {
		scopesJSON, _ := json.Marshal(req.Scopes)
		args = append(args, string(scopesJSON))
		updates = append(updates, "scopes_json = $"+strconv.Itoa(len(args))+"::jsonb")
	}
	status := strings.TrimSpace(strings.ToLower(req.Status))
	if status != "" {
		if status != "revoked" {
			writeAPIError(w, http.StatusBadRequest, "invalid_status", "only revoked status can be set through user API key update")
			return
		}
		updates = append(updates, "status = 'revoked'", "revoked_at = now()")
	}
	if len(updates) == 0 {
		key, err := s.apiKeyForUser(ctx, userID, keyID)
		if err != nil {
			writeAPIError(w, http.StatusNotFound, "api_key_not_found", "API key was not found")
			return
		}
		writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-api-key-v1", "key": key, "updated": false, "valuesRedacted": true})
		return
	}
	query := `UPDATE hwlab_api_keys SET ` + strings.Join(updates, ", ") + ` WHERE id = $1 AND user_id = $2 RETURNING id, name, key_prefix, scopes_json::text, status, created_at, last_used_at, revoked_at`
	key, err := scanAPIKey(s.db.QueryRowContext(ctx, query, args...))
	if errors.Is(err, sql.ErrNoRows) {
		writeAPIError(w, http.StatusNotFound, "api_key_not_found", "API key was not found")
		return
	}
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "api_key_update_failed", "could not update API key")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-api-key-v1", "key": key, "updated": true, "valuesRedacted": true})
}

func (s *Server) handleRevokeAPIKey(w http.ResponseWriter, r *http.Request, userID, keyID string) {
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	key, err := scanAPIKey(s.db.QueryRowContext(ctx, `UPDATE hwlab_api_keys SET status = 'revoked', revoked_at = COALESCE(revoked_at, now()) WHERE id = $1 AND user_id = $2 RETURNING id, name, key_prefix, scopes_json::text, status, created_at, last_used_at, revoked_at`, keyID, userID))
	if errors.Is(err, sql.ErrNoRows) {
		writeAPIError(w, http.StatusNotFound, "api_key_not_found", "API key was not found")
		return
	}
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "api_key_revoke_failed", "could not revoke API key")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-api-key-v1", "key": key, "revoked": true, "valuesRedacted": true})
}

func (s *Server) handleIntrospect(w http.ResponseWriter, r *http.Request) {
	if !s.internalAllowed(w, r) || !s.databaseAvailable(w) {
		return
	}
	var req struct {
		Token  string `json:"token"`
		APIKey string `json:"apiKey"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	token := first(req.Token, req.APIKey)
	principal, err := s.principalFromToken(r.Context(), token)
	if err != nil {
		writeJSON(w, http.StatusOK, map[string]any{"active": false})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"active": true, "principal": principal})
}

func (s *Server) handleBillingPreflight(w http.ResponseWriter, r *http.Request) {
	if !s.internalAllowed(w, r) || !s.databaseAvailable(w) {
		return
	}
	var req struct {
		Token            string         `json:"token"`
		APIKey           string         `json:"apiKey"`
		UserID           string         `json:"userId"`
		ServiceID        string         `json:"serviceId"`
		EstimatedCredits int64          `json:"estimatedCredits"`
		EstimatedTokens  int64          `json:"estimatedTokens"`
		IdempotencyKey   string         `json:"idempotencyKey"`
		Metadata         map[string]any `json:"metadata"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	principal, err := s.principalForBilling(r.Context(), r, req.Token, req.APIKey, req.UserID)
	if err != nil {
		writeAPIError(w, http.StatusUnauthorized, "invalid_subject", "billing subject is not active")
		return
	}
	service := strings.TrimSpace(req.ServiceID)
	if service == "" {
		writeAPIError(w, http.StatusBadRequest, "missing_service", "serviceId is required")
		return
	}
	estimated := s.estimateCredits(req.EstimatedCredits, req.EstimatedTokens)
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	reservation, err := s.reserveCredits(ctx, principal.UserID, principal.KeyID, service, estimated, req.EstimatedTokens, strings.TrimSpace(req.IdempotencyKey), req.Metadata)
	if err != nil {
		if errors.Is(err, errInsufficientCredits) {
			writeJSON(w, http.StatusPaymentRequired, map[string]any{"allowed": false, "reason": "insufficient_credits"})
			return
		}
		var limitErr billingLimitError
		if errors.As(err, &limitErr) {
			writeJSON(w, limitErr.Status, map[string]any{"allowed": false, "reason": limitErr.Code, "error": apiError{Code: limitErr.Code, Message: limitErr.Message}})
			return
		}
		writeAPIError(w, http.StatusInternalServerError, "billing_preflight_failed", "could not reserve credits")
		return
	}
	writeJSON(w, http.StatusOK, reservation)
}

func (s *Server) handleBillingRecord(w http.ResponseWriter, r *http.Request) {
	if !s.internalAllowed(w, r) || !s.databaseAvailable(w) {
		return
	}
	var req struct {
		ReservationID  string         `json:"reservationId"`
		Token          string         `json:"token"`
		APIKey         string         `json:"apiKey"`
		UserID         string         `json:"userId"`
		ServiceID      string         `json:"serviceId"`
		UsedCredits    int64          `json:"usedCredits"`
		UsedTokens     int64          `json:"usedTokens"`
		IdempotencyKey string         `json:"idempotencyKey"`
		Metadata       map[string]any `json:"metadata"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	result, err := s.recordUsage(ctx, r, req.ReservationID, req.Token, req.APIKey, req.UserID, req.ServiceID, req.UsedCredits, req.UsedTokens, req.IdempotencyKey, req.Metadata)
	if err != nil {
		writeAPIError(w, http.StatusBadRequest, "billing_record_failed", err.Error())
		return
	}
	writeJSON(w, http.StatusOK, result)
}

func (s *Server) handleBillingRelease(w http.ResponseWriter, r *http.Request) {
	if !s.internalAllowed(w, r) || !s.databaseAvailable(w) {
		return
	}
	var req struct {
		ReservationID  string         `json:"reservationId"`
		ServiceID      string         `json:"serviceId"`
		Reason         string         `json:"reason"`
		IdempotencyKey string         `json:"idempotencyKey"`
		Metadata       map[string]any `json:"metadata"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	result, err := s.releaseReservation(ctx, req.ReservationID, req.ServiceID, req.Reason, req.IdempotencyKey, req.Metadata)
	if err != nil {
		writeAPIError(w, http.StatusBadRequest, "billing_release_failed", err.Error())
		return
	}
	writeJSON(w, http.StatusOK, result)
}

func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
	if !s.internalAllowed(w, r) || !s.databaseAvailable(w) {
		return
	}
	switch r.Method {
	case http.MethodGet:
		s.handleAdminUserList(w, r)
	case http.MethodPost:
		s.handleAdminCreateUser(w, r)
	default:
		w.Header().Set("Allow", "GET, POST")
		writeAPIError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
	}
}

func (s *Server) handleAdminUserByID(w http.ResponseWriter, r *http.Request) {
	if !s.internalAllowed(w, r) || !s.databaseAvailable(w) {
		return
	}
	userID := strings.TrimSpace(strings.TrimPrefix(r.URL.Path, "/internal/admin/users/"))
	if userID == "" || strings.Contains(userID, "/") {
		writeAPIError(w, http.StatusNotFound, "user_not_found", "user not found")
		return
	}
	switch r.Method {
	case http.MethodGet:
		s.handleAdminUserDetail(w, r, userID)
	case http.MethodPatch:
		s.handleAdminUpdateUser(w, r, userID)
	default:
		w.Header().Set("Allow", "GET, PATCH")
		writeAPIError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
	}
}

func (s *Server) handleAdminUserList(w http.ResponseWriter, r *http.Request) {
	query := adminUsersQueryFromRequest(r)
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	users, total, err := s.adminUsers(ctx, query)
	if err != nil {
		s.logDatabaseError("admin_users_list", err)
		writeAPIError(w, http.StatusInternalServerError, "admin_users_failed", "could not load users")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"contractVersion": "user-billing-admin-users-v1",
		"users":           users,
		"count":           len(users),
		"total":           total,
		"pagination": map[string]any{
			"page":     query.Page,
			"pageSize": query.PageSize,
			"offset":   (query.Page - 1) * query.PageSize,
		},
		"filters": map[string]any{"search": query.Search, "status": query.Status, "role": query.Role},
		"state": map[string]any{
			"stateless":      true,
			"stateAuthority": s.config.StateAuthority,
			"database":       map[string]any{"authority": s.config.ExternalDatabaseLabel},
			"redis":          map[string]any{"role": "cache-only"},
		},
		"valuesRedacted": true,
	})
}

func (s *Server) handleAdminUserDetail(w http.ResponseWriter, r *http.Request, userID string) {
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	detail, err := s.adminUserDetail(ctx, userID)
	if errors.Is(err, sql.ErrNoRows) {
		writeAPIError(w, http.StatusNotFound, "user_not_found", "user not found")
		return
	}
	if err != nil {
		s.logDatabaseError("admin_user_detail", err)
		writeAPIError(w, http.StatusInternalServerError, "admin_user_detail_failed", "could not load user detail")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-billing-admin-user-v1", "detail": detail, "valuesRedacted": true})
}

func (s *Server) handleAdminCreateUser(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Email          string `json:"email"`
		Username       string `json:"username"`
		DisplayName    string `json:"displayName"`
		Password       string `json:"password"`
		Role           string `json:"role"`
		Status         string `json:"status"`
		InitialCredits int64  `json:"initialCredits"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	email := strings.ToLower(strings.TrimSpace(req.Email))
	username := normalizeUsername(req.Username)
	role := normalizeRole(req.Role, "user")
	status := normalizeUserStatus(req.Status, "active")
	if email == "" || !strings.Contains(email, "@") || username == "" || len(req.Password) < 10 || role == "" || status == "" {
		writeAPIError(w, http.StatusBadRequest, "invalid_admin_user", "email, username, role, status and a password with at least 10 characters are required")
		return
	}
	passwordHash, err := hashPassword(req.Password)
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "password_hash_failed", "could not hash password")
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "transaction_failed", "could not start transaction")
		return
	}
	defer tx.Rollback()
	user := User{ID: newID("usr"), Email: email, Username: username, DisplayName: strings.TrimSpace(req.DisplayName), Status: status, Role: role}
	err = tx.QueryRowContext(ctx, `INSERT INTO hwlab_users (id, email, username, display_name, password_hash, status, role) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING email_verified, created_at`, user.ID, user.Email, user.Username, user.DisplayName, passwordHash, status, role).Scan(&user.EmailVerified, &user.CreatedAt)
	if err != nil {
		if isDuplicate(err) {
			writeAPIError(w, http.StatusConflict, "user_exists", "email or username already exists")
			return
		}
		writeAPIError(w, http.StatusInternalServerError, "user_create_failed", "could not create user")
		return
	}
	_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_credit_accounts (user_id, balance_credits, reserved_credits, plan_id) VALUES ($1, $2, 0, 'default')`, user.ID, nonNegative(req.InitialCredits))
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "credit_account_failed", "could not create credit account")
		return
	}
	if req.InitialCredits > 0 {
		_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_credit_ledger (id, user_id, delta_credits, balance_before, balance_after, kind, reason, source, status, metadata) VALUES ($1, $2, $3, 0, $3, 'admin_grant', 'admin_create_user_initial_credit', 'admin', 'applied', '{}'::jsonb)`, newID("led"), user.ID, req.InitialCredits)
		if err != nil {
			writeAPIError(w, http.StatusInternalServerError, "credit_ledger_failed", "could not write initial credit ledger")
			return
		}
	}
	if err := tx.Commit(); err != nil {
		writeAPIError(w, http.StatusInternalServerError, "transaction_commit_failed", "could not commit user creation")
		return
	}
	writeJSON(w, http.StatusCreated, map[string]any{"contractVersion": "user-billing-admin-user-v1", "created": true, "user": user, "valuesRedacted": true})
}

func (s *Server) handleAdminUpdateUser(w http.ResponseWriter, r *http.Request, userID string) {
	var req struct {
		Email       *string `json:"email"`
		Username    *string `json:"username"`
		DisplayName *string `json:"displayName"`
		Password    *string `json:"password"`
		Role        *string `json:"role"`
		Status      *string `json:"status"`
		PlanID      *string `json:"planId"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	updates := []string{}
	args := []any{userID}
	if req.Email != nil {
		email := strings.ToLower(strings.TrimSpace(*req.Email))
		if email == "" || !strings.Contains(email, "@") {
			writeAPIError(w, http.StatusBadRequest, "invalid_email", "email is invalid")
			return
		}
		args = append(args, email)
		updates = append(updates, "email = $"+strconv.Itoa(len(args)))
	}
	if req.Username != nil {
		username := normalizeUsername(*req.Username)
		if username == "" {
			writeAPIError(w, http.StatusBadRequest, "invalid_username", "username is invalid")
			return
		}
		args = append(args, username)
		updates = append(updates, "username = $"+strconv.Itoa(len(args)))
	}
	if req.DisplayName != nil {
		args = append(args, strings.TrimSpace(*req.DisplayName))
		updates = append(updates, "display_name = $"+strconv.Itoa(len(args)))
	}
	if req.Role != nil {
		role := normalizeRole(*req.Role, "")
		if role == "" {
			writeAPIError(w, http.StatusBadRequest, "invalid_role", "role must be user or admin")
			return
		}
		args = append(args, role)
		updates = append(updates, "role = $"+strconv.Itoa(len(args)))
	}
	if req.Status != nil {
		status := normalizeUserStatus(*req.Status, "")
		if status == "" {
			writeAPIError(w, http.StatusBadRequest, "invalid_status", "status must be active, disabled or pending")
			return
		}
		args = append(args, status)
		updates = append(updates, "status = $"+strconv.Itoa(len(args)))
	}
	if req.Password != nil {
		if len(*req.Password) < 10 {
			writeAPIError(w, http.StatusBadRequest, "invalid_password", "password must be at least 10 characters")
			return
		}
		passwordHash, err := hashPassword(*req.Password)
		if err != nil {
			writeAPIError(w, http.StatusInternalServerError, "password_hash_failed", "could not hash password")
			return
		}
		args = append(args, passwordHash)
		updates = append(updates, "password_hash = $"+strconv.Itoa(len(args)))
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	planID := ""
	if req.PlanID != nil {
		planID = strings.TrimSpace(*req.PlanID)
		if planID == "" {
			writeAPIError(w, http.StatusBadRequest, "invalid_plan", "planId is required when updating a billing plan")
			return
		}
	}
	if len(updates) == 0 && planID == "" {
		detail, err := s.adminUserDetail(ctx, userID)
		if errors.Is(err, sql.ErrNoRows) {
			writeAPIError(w, http.StatusNotFound, "user_not_found", "user not found")
			return
		}
		if err != nil {
			writeAPIError(w, http.StatusInternalServerError, "admin_user_detail_failed", "could not load user detail")
			return
		}
		writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-billing-admin-user-v1", "updated": false, "detail": detail, "valuesRedacted": true})
		return
	}
	if len(updates) == 0 {
		detail, err := s.updateUserPlan(ctx, userID, planID)
		if errors.Is(err, sql.ErrNoRows) {
			writeAPIError(w, http.StatusNotFound, "user_or_plan_not_found", "user or plan not found")
			return
		}
		if err != nil {
			writeAPIError(w, http.StatusBadRequest, "plan_update_failed", err.Error())
			return
		}
		writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-billing-admin-user-v1", "updated": true, "detail": detail, "valuesRedacted": true})
		return
	}
	query := `UPDATE hwlab_users SET ` + strings.Join(updates, ", ") + `, updated_at = now() WHERE id = $1 RETURNING id, email, username, display_name, status, role, email_verified, created_at`
	var user User
	err := s.db.QueryRowContext(ctx, query, args...).Scan(&user.ID, &user.Email, &user.Username, &user.DisplayName, &user.Status, &user.Role, &user.EmailVerified, &user.CreatedAt)
	if errors.Is(err, sql.ErrNoRows) {
		writeAPIError(w, http.StatusNotFound, "user_not_found", "user not found")
		return
	}
	if err != nil {
		if isDuplicate(err) {
			writeAPIError(w, http.StatusConflict, "user_exists", "email or username already exists")
			return
		}
		writeAPIError(w, http.StatusInternalServerError, "user_update_failed", "could not update user")
		return
	}
	if planID != "" {
		if _, err := s.updateUserPlan(ctx, userID, planID); err != nil {
			writeAPIError(w, http.StatusBadRequest, "plan_update_failed", err.Error())
			return
		}
	}
	writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-billing-admin-user-v1", "updated": true, "user": user, "valuesRedacted": true})
}

func (s *Server) handleAdminCreditAdjust(w http.ResponseWriter, r *http.Request) {
	if !s.internalAllowed(w, r) || !s.databaseAvailable(w) {
		return
	}
	var req struct {
		UserID         string         `json:"userId"`
		DeltaCredits   int64          `json:"deltaCredits"`
		Reason         string         `json:"reason"`
		IdempotencyKey string         `json:"idempotencyKey"`
		Metadata       map[string]any `json:"metadata"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	if strings.TrimSpace(req.UserID) == "" || req.DeltaCredits == 0 {
		writeAPIError(w, http.StatusBadRequest, "invalid_adjustment", "userId and non-zero deltaCredits are required")
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	adjustment, err := s.adjustCredits(ctx, req.UserID, req.DeltaCredits, "admin_adjust", req.Reason, req.IdempotencyKey, req.Metadata)
	if err != nil {
		writeAPIError(w, http.StatusBadRequest, "credit_adjust_failed", err.Error())
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"userId": req.UserID, "balance": adjustment.BalanceAfter, "adjustment": adjustment, "valuesRedacted": true})
}

func (s *Server) handleAdminUserStatus(w http.ResponseWriter, r *http.Request) {
	if !s.internalAllowed(w, r) || !s.databaseAvailable(w) {
		return
	}
	var req struct {
		UserID string `json:"userId"`
		Status string `json:"status"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	if req.Status != "active" && req.Status != "disabled" && req.Status != "pending" {
		writeAPIError(w, http.StatusBadRequest, "invalid_status", "status must be active, disabled or pending")
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	res, err := s.db.ExecContext(ctx, `UPDATE hwlab_users SET status = $2, updated_at = now() WHERE id = $1`, req.UserID, req.Status)
	if err != nil {
		writeAPIError(w, http.StatusInternalServerError, "status_update_failed", "could not update user status")
		return
	}
	affected, _ := res.RowsAffected()
	if affected == 0 {
		writeAPIError(w, http.StatusNotFound, "user_not_found", "user not found")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"userId": req.UserID, "status": req.Status})
}

func (s *Server) handleAdminBillingSummary(w http.ResponseWriter, r *http.Request) {
	if !s.internalAllowed(w, r) || !s.databaseAvailable(w) {
		return
	}
	limit := queryLimit(r, "limit", 50, 100)
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()

	users, err := s.adminBillingUsers(ctx, limit)
	if err != nil {
		s.logDatabaseError("admin_billing_summary_users", err)
		writeAPIError(w, http.StatusInternalServerError, "admin_billing_summary_failed", "could not load admin billing summary")
		return
	}
	totals, err := s.adminBillingTotals(ctx)
	if err != nil {
		s.logDatabaseError("admin_billing_summary_totals", err)
		writeAPIError(w, http.StatusInternalServerError, "admin_billing_summary_failed", "could not load admin billing totals")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"contractVersion": "user-billing-admin-summary-v1",
		"serviceId":       serviceID,
		"status":          "ok",
		"users":           users,
		"count":           len(users),
		"limit":           limit,
		"totals":          totals,
		"state": map[string]any{
			"stateless":      true,
			"stateAuthority": s.config.StateAuthority,
			"database":       map[string]any{"authority": s.config.ExternalDatabaseLabel},
			"redis":          map[string]any{"role": "cache-only"},
		},
		"valuesRedacted": true,
	})
}

func (s *Server) handleAdminBillingPlans(w http.ResponseWriter, r *http.Request) {
	if !s.internalAllowed(w, r) || !s.databaseAvailable(w) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	plans, err := s.billingPlans(ctx)
	if err != nil {
		s.logDatabaseError("admin_billing_plans", err)
		writeAPIError(w, http.StatusInternalServerError, "admin_billing_plans_failed", "could not load billing plans")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"contractVersion": "user-billing-admin-plans-v1", "plans": plans, "count": len(plans), "limitSemantics": "zero-means-unlimited", "valuesRedacted": true})
}

var errInsufficientCredits = errors.New("insufficient credits")

type billingLimitError struct {
	Code    string
	Message string
	Status  int
}

func (err billingLimitError) Error() string { return err.Message }

func (s *Server) reserveCredits(ctx context.Context, userID, apiKeyID, service string, credits, tokens int64, idempotencyKey string, metadata map[string]any) (map[string]any, error) {
	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()
	if idempotencyKey != "" {
		var existingID, status string
		var estimated int64
		var resourceType string
		err := tx.QueryRowContext(ctx, `SELECT id, status, estimated_credits, resource_type FROM hwlab_billing_reservations WHERE idempotency_key = $1`, idempotencyKey).Scan(&existingID, &status, &estimated, &resourceType)
		if err == nil {
			return map[string]any{"allowed": status == "reserved", "reservationId": existingID, "estimatedCredits": estimated, "resourceType": resourceType, "status": status, "idempotent": true}, tx.Commit()
		}
		if !errors.Is(err, sql.ErrNoRows) {
			return nil, err
		}
	}
	var balance, reserved int64
	var planID string
	if err := tx.QueryRowContext(ctx, `SELECT balance_credits, reserved_credits, plan_id FROM hwlab_credit_accounts WHERE user_id = $1 FOR UPDATE`, userID).Scan(&balance, &reserved, &planID); err != nil {
		return nil, err
	}
	resourceType := resourceTypeForService(service)
	entitlement, err := s.evaluateResourceEntitlement(ctx, tx, userID, planID, resourceType, credits)
	if err != nil {
		return nil, err
	}
	available := balance - reserved
	if available < credits {
		return nil, errInsufficientCredits
	}
	reservationID := newID("res")
	metadataJSON := jsonObject(metadata)
	expiresAt := time.Now().UTC().Add(s.config.ReservationTTL)
	_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_billing_reservations (id, user_id, service_id, resource_type, estimated_credits, estimated_tokens, api_key_id, idempotency_key, metadata, expires_at) VALUES ($1, $2, $3, $4, $5, $6, nullif($7, ''), nullif($8, ''), $9::jsonb, $10)`, reservationID, userID, service, resourceType, credits, nonNegative(tokens), apiKeyID, idempotencyKey, metadataJSON, expiresAt)
	if err != nil {
		return nil, err
	}
	_, err = tx.ExecContext(ctx, `UPDATE hwlab_credit_accounts SET reserved_credits = reserved_credits + $2, updated_at = now() WHERE user_id = $1`, userID, credits)
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return map[string]any{"allowed": true, "reservationId": reservationID, "estimatedCredits": credits, "estimatedTokens": tokens, "resourceType": resourceType, "planId": planID, "entitlement": entitlement, "availableBefore": available, "expiresAt": expiresAt}, nil
}

func (s *Server) recordUsage(ctx context.Context, r *http.Request, reservationID, token, apiKey, userID, serviceID string, usedCredits, usedTokens int64, idempotencyKey string, metadata map[string]any) (map[string]any, error) {
	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()
	if idempotencyKey != "" {
		var existingID string
		err := tx.QueryRowContext(ctx, `SELECT id FROM hwlab_usage_records WHERE idempotency_key = $1`, idempotencyKey).Scan(&existingID)
		if err == nil {
			return map[string]any{"recordId": existingID, "idempotent": true}, tx.Commit()
		}
		if !errors.Is(err, sql.ErrNoRows) {
			return nil, err
		}
	}
	var reserved int64
	apiKeyID := ""
	resourceType := ""
	if reservationID != "" {
		var status string
		err := tx.QueryRowContext(ctx, `SELECT user_id, service_id, resource_type, estimated_credits, status, COALESCE(api_key_id, '') FROM hwlab_billing_reservations WHERE id = $1 FOR UPDATE`, reservationID).Scan(&userID, &serviceID, &resourceType, &reserved, &status, &apiKeyID)
		if err != nil {
			return nil, err
		}
		if status != "reserved" {
			return nil, fmt.Errorf("reservation status is %s", status)
		}
	} else {
		principal, err := s.principalForBilling(ctx, r, token, apiKey, userID)
		if err != nil {
			return nil, err
		}
		userID = principal.UserID
		apiKeyID = principal.KeyID
		serviceID = strings.TrimSpace(serviceID)
		resourceType = resourceTypeForService(serviceID)
	}
	if serviceID == "" {
		return nil, errors.New("serviceId is required")
	}
	if resourceType == "" {
		resourceType = resourceTypeForService(serviceID)
	}
	credits := s.estimateCredits(usedCredits, usedTokens)
	var balance, accountReserved int64
	var planID string
	if err := tx.QueryRowContext(ctx, `SELECT balance_credits, reserved_credits, plan_id FROM hwlab_credit_accounts WHERE user_id = $1 FOR UPDATE`, userID).Scan(&balance, &accountReserved, &planID); err != nil {
		return nil, err
	}
	if reservationID == "" {
		if _, err := s.evaluateResourceEntitlement(ctx, tx, userID, planID, resourceType, credits); err != nil {
			return nil, err
		}
	}
	if reservationID == "" && balance-accountReserved < credits {
		return nil, errInsufficientCredits
	}
	newReserved := accountReserved - reserved
	if newReserved < 0 {
		newReserved = 0
	}
	newBalance := balance - credits
	if newBalance < 0 {
		return nil, errInsufficientCredits
	}
	_, err = tx.ExecContext(ctx, `UPDATE hwlab_credit_accounts SET balance_credits = $2, reserved_credits = $3, updated_at = now() WHERE user_id = $1`, userID, newBalance, newReserved)
	if err != nil {
		return nil, err
	}
	recordID := newID("use")
	metadataJSON := jsonObject(metadata)
	_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_usage_records (id, reservation_id, user_id, service_id, resource_type, api_key_id, quantity, credits, status, idempotency_key, metadata) VALUES ($1, nullif($2, ''), $3, $4, $5, nullif($6, ''), $7, $8, 'recorded', nullif($9, ''), $10::jsonb)`, recordID, reservationID, userID, serviceID, resourceType, apiKeyID, nonNegative(usedTokens), credits, idempotencyKey, metadataJSON)
	if err != nil {
		return nil, err
	}
	usageLedgerKey := ""
	if idempotencyKey != "" {
		usageLedgerKey = "usage:" + idempotencyKey
	}
	_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_credit_ledger (id, user_id, delta_credits, balance_before, balance_after, kind, reason, source, status, idempotency_key, metadata) VALUES ($1, $2, $3, $4, $5, 'usage', $6, 'usage', 'applied', nullif($7, ''), $8::jsonb)`, newID("led"), userID, -credits, balance, newBalance, serviceID, usageLedgerKey, metadataJSON)
	if err != nil {
		return nil, err
	}
	if reservationID != "" {
		_, err = tx.ExecContext(ctx, `UPDATE hwlab_billing_reservations SET status = 'completed', updated_at = now() WHERE id = $1`, reservationID)
		if err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return map[string]any{"recordId": recordID, "userId": userID, "serviceId": serviceID, "resourceType": resourceType, "credits": credits, "balance": newBalance}, nil
}

func (s *Server) releaseReservation(ctx context.Context, reservationID, serviceID, reason, idempotencyKey string, metadata map[string]any) (map[string]any, error) {
	reservationID = strings.TrimSpace(reservationID)
	if reservationID == "" {
		return nil, errors.New("reservationId is required")
	}
	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()
	var userID, actualServiceID, resourceType, status string
	var reserved int64
	err = tx.QueryRowContext(ctx, `SELECT user_id, service_id, resource_type, estimated_credits, status FROM hwlab_billing_reservations WHERE id = $1 FOR UPDATE`, reservationID).Scan(&userID, &actualServiceID, &resourceType, &reserved, &status)
	if err != nil {
		return nil, err
	}
	serviceID = strings.TrimSpace(serviceID)
	if serviceID != "" && serviceID != actualServiceID {
		return nil, fmt.Errorf("reservation service is %s", actualServiceID)
	}
	if status == "cancelled" || status == "expired" {
		return map[string]any{"released": true, "reservationId": reservationID, "userId": userID, "serviceId": actualServiceID, "resourceType": resourceType, "releasedCredits": int64(0), "status": status, "idempotent": true}, tx.Commit()
	}
	if status != "reserved" {
		return nil, fmt.Errorf("reservation status is %s", status)
	}
	var accountReserved int64
	if err := tx.QueryRowContext(ctx, `SELECT reserved_credits FROM hwlab_credit_accounts WHERE user_id = $1 FOR UPDATE`, userID).Scan(&accountReserved); err != nil {
		return nil, err
	}
	newReserved := accountReserved - reserved
	if newReserved < 0 {
		newReserved = 0
	}
	_, err = tx.ExecContext(ctx, `UPDATE hwlab_credit_accounts SET reserved_credits = $2, updated_at = now() WHERE user_id = $1`, userID, newReserved)
	if err != nil {
		return nil, err
	}
	releaseMetadata := map[string]any{
		"releaseReason":         strings.TrimSpace(reason),
		"releaseIdempotency":    strings.TrimSpace(idempotencyKey),
		"releaseValuesRedacted": true,
	}
	for key, value := range metadata {
		releaseMetadata[key] = value
	}
	_, err = tx.ExecContext(ctx, `UPDATE hwlab_billing_reservations SET status = 'cancelled', metadata = COALESCE(metadata, '{}'::jsonb) || $2::jsonb, updated_at = now() WHERE id = $1`, reservationID, jsonObject(releaseMetadata))
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return map[string]any{"released": true, "reservationId": reservationID, "userId": userID, "serviceId": actualServiceID, "resourceType": resourceType, "releasedCredits": reserved, "status": "cancelled"}, nil
}

func (s *Server) adjustCredits(ctx context.Context, userID string, delta int64, kind, reason, idempotencyKey string, metadata map[string]any) (creditAdjustResult, error) {
	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return creditAdjustResult{}, err
	}
	defer tx.Rollback()
	operatorUserID, operatorUsername, _ := operatorFields(metadata)
	if idempotencyKey != "" {
		var result creditAdjustResult
		var before sql.NullInt64
		err := tx.QueryRowContext(ctx, `SELECT id, user_id, COALESCE(balance_before, balance_after - delta_credits), balance_after, delta_credits, kind, reason, source, status FROM hwlab_credit_ledger WHERE idempotency_key = $1`, idempotencyKey).Scan(&result.LedgerID, &result.UserID, &before, &result.BalanceAfter, &result.DeltaCredits, &result.Kind, &result.Reason, &result.Source, &result.Status)
		if err == nil {
			if before.Valid {
				result.BalanceBefore = before.Int64
			}
			result.IdempotencyKey = idempotencyKey
			result.Idempotent = true
			return result, tx.Commit()
		}
		if !errors.Is(err, sql.ErrNoRows) {
			return creditAdjustResult{}, err
		}
	}
	var balance int64
	if err := tx.QueryRowContext(ctx, `SELECT balance_credits FROM hwlab_credit_accounts WHERE user_id = $1 FOR UPDATE`, userID).Scan(&balance); err != nil {
		return creditAdjustResult{}, err
	}
	newBalance := balance + delta
	if newBalance < 0 {
		return creditAdjustResult{}, errInsufficientCredits
	}
	_, err = tx.ExecContext(ctx, `UPDATE hwlab_credit_accounts SET balance_credits = $2, updated_at = now() WHERE user_id = $1`, userID, newBalance)
	if err != nil {
		return creditAdjustResult{}, err
	}
	ledgerID := newID("led")
	_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_credit_ledger (id, user_id, delta_credits, balance_before, balance_after, kind, reason, source, status, idempotency_key, operator_user_id, operator_username, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7, 'admin', 'applied', nullif($8, ''), nullif($9, ''), nullif($10, ''), $11::jsonb)`, ledgerID, userID, delta, balance, newBalance, kind, reason, idempotencyKey, operatorUserID, operatorUsername, jsonObject(metadata))
	if err != nil {
		return creditAdjustResult{}, err
	}
	result := creditAdjustResult{UserID: userID, BalanceBefore: balance, BalanceAfter: newBalance, DeltaCredits: delta, LedgerID: ledgerID, Kind: kind, Reason: reason, Source: "admin", Status: "applied", IdempotencyKey: idempotencyKey, Idempotent: false}
	return result, tx.Commit()
}

type txQueryer interface {
	QueryContext(context.Context, string, ...any) (*sql.Rows, error)
	QueryRowContext(context.Context, string, ...any) *sql.Row
}

func (s *Server) evaluateResourceEntitlement(ctx context.Context, tx txQueryer, userID, planID, resourceType string, requestedCredits int64) (resourceEntitlementSummary, error) {
	plan, err := s.planByID(ctx, tx, planID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return resourceEntitlementSummary{}, billingLimitError{Code: "plan_not_found", Message: "billing plan is not active", Status: http.StatusForbidden}
		}
		return resourceEntitlementSummary{}, err
	}
	if plan.Status != "active" {
		return resourceEntitlementSummary{}, billingLimitError{Code: "plan_disabled", Message: "billing plan is disabled", Status: http.StatusForbidden}
	}
	entitlements, err := s.resourceEntitlements(ctx, tx, planID, userID)
	if err != nil {
		return resourceEntitlementSummary{}, err
	}
	var entitlement resourceEntitlementSummary
	found := false
	for _, item := range entitlements {
		if item.ResourceType == resourceType {
			entitlement = item
			found = true
			break
		}
	}
	if !found || !entitlement.Enabled {
		return resourceEntitlementSummary{}, billingLimitError{Code: "resource_not_entitled", Message: "resource is not enabled for this billing plan", Status: http.StatusForbidden}
	}
	if entitlement.MaxConcurrentReservations > 0 && entitlement.ActiveReservations >= entitlement.MaxConcurrentReservations {
		return entitlement, billingLimitError{Code: "resource_concurrency_exceeded", Message: "resource concurrency limit exceeded", Status: http.StatusTooManyRequests}
	}
	if entitlement.RPMLimit > 0 && entitlement.RPMUsed >= entitlement.RPMLimit {
		return entitlement, billingLimitError{Code: "resource_rpm_exceeded", Message: "resource RPM limit exceeded", Status: http.StatusTooManyRequests}
	}
	if entitlement.MonthlyQuotaCredits > 0 && entitlement.MonthlyUsageCredits+requestedCredits > entitlement.MonthlyQuotaCredits {
		return entitlement, billingLimitError{Code: "resource_monthly_quota_exceeded", Message: "resource monthly quota exceeded", Status: http.StatusPaymentRequired}
	}
	return entitlement, nil
}

func (s *Server) planByID(ctx context.Context, queryer txQueryer, planID string) (billingPlanSummary, error) {
	planID = strings.TrimSpace(planID)
	if planID == "" {
		planID = "default"
	}
	var plan billingPlanSummary
	var metadataRaw string
	err := queryer.QueryRowContext(ctx, `SELECT id, display_name, status, description, metadata::text FROM hwlab_billing_plans WHERE id = $1`, planID).Scan(&plan.ID, &plan.DisplayName, &plan.Status, &plan.Description, &metadataRaw)
	if err != nil {
		return plan, err
	}
	plan.Metadata = parseJSONMap(metadataRaw)
	return plan, nil
}

func (s *Server) billingPlans(ctx context.Context) ([]billingPlanDetail, error) {
	rows, err := s.db.QueryContext(ctx, `SELECT id, display_name, status, description, metadata::text FROM hwlab_billing_plans ORDER BY id ASC`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	plans := []billingPlanDetail{}
	for rows.Next() {
		var detail billingPlanDetail
		var metadataRaw string
		if err := rows.Scan(&detail.Plan.ID, &detail.Plan.DisplayName, &detail.Plan.Status, &detail.Plan.Description, &metadataRaw); err != nil {
			return nil, err
		}
		detail.Plan.Metadata = parseJSONMap(metadataRaw)
		plans = append(plans, detail)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	for i := range plans {
		entitlements, err := s.resourceEntitlements(ctx, s.db, plans[i].Plan.ID, "")
		if err != nil {
			return nil, err
		}
		plans[i].Entitlements = entitlements
	}
	return plans, nil
}

func (s *Server) updateUserPlan(ctx context.Context, userID, planID string) (adminUserDetail, error) {
	planID = strings.TrimSpace(planID)
	if planID == "" {
		return adminUserDetail{}, errors.New("planId is required")
	}
	if _, err := s.planByID(ctx, s.db, planID); err != nil {
		return adminUserDetail{}, err
	}
	res, err := s.db.ExecContext(ctx, `UPDATE hwlab_credit_accounts SET plan_id = $2, updated_at = now() WHERE user_id = $1`, userID, planID)
	if err != nil {
		return adminUserDetail{}, err
	}
	affected, _ := res.RowsAffected()
	if affected == 0 {
		return adminUserDetail{}, sql.ErrNoRows
	}
	return s.adminUserDetail(ctx, userID)
}

func (s *Server) resourceEntitlements(ctx context.Context, queryer txQueryer, planID, userID string) ([]resourceEntitlementSummary, error) {
	rows, err := queryer.QueryContext(ctx, `
SELECT id, plan_id, resource_type, enabled, monthly_quota_credits, max_concurrent_reservations, rpm_limit, metadata::text
FROM hwlab_resource_entitlements
WHERE plan_id = $1
ORDER BY resource_type ASC`, planID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []resourceEntitlementSummary{}
	for rows.Next() {
		var item resourceEntitlementSummary
		var metadataRaw string
		if err := rows.Scan(&item.ID, &item.PlanID, &item.ResourceType, &item.Enabled, &item.MonthlyQuotaCredits, &item.MaxConcurrentReservations, &item.RPMLimit, &metadataRaw); err != nil {
			return nil, err
		}
		item.Metadata = parseJSONMap(metadataRaw)
		item.LimitSemantics = "zero-means-unlimited"
		item.MonthlyQuotaUnlimited = item.MonthlyQuotaCredits == 0
		item.ConcurrencyUnlimited = item.MaxConcurrentReservations == 0
		item.RPMUnlimited = item.RPMLimit == 0
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	if strings.TrimSpace(userID) == "" || len(items) == 0 {
		return items, nil
	}
	for i := range items {
		if err := s.populateEntitlementUsage(ctx, queryer, userID, &items[i]); err != nil {
			return nil, err
		}
	}
	return items, nil
}

func (s *Server) populateEntitlementUsage(ctx context.Context, queryer txQueryer, userID string, item *resourceEntitlementSummary) error {
	monthStart := time.Now().UTC().Format("2006-01-02")
	if err := queryer.QueryRowContext(ctx, `SELECT COALESCE(SUM(credits), 0) FROM hwlab_usage_records WHERE user_id = $1 AND resource_type = $2 AND created_at >= date_trunc('month', $3::timestamptz)`, userID, item.ResourceType, monthStart).Scan(&item.MonthlyUsageCredits); err != nil {
		return err
	}
	if err := queryer.QueryRowContext(ctx, `SELECT COUNT(*) FROM hwlab_billing_reservations WHERE user_id = $1 AND resource_type = $2 AND status = 'reserved' AND expires_at > now()`, userID, item.ResourceType).Scan(&item.ActiveReservations); err != nil {
		return err
	}
	if err := queryer.QueryRowContext(ctx, `SELECT COUNT(*) FROM hwlab_billing_reservations WHERE user_id = $1 AND resource_type = $2 AND created_at >= now() - interval '1 minute'`, userID, item.ResourceType).Scan(&item.RPMUsed); err != nil {
		return err
	}
	if item.MonthlyQuotaCredits > 0 {
		item.MonthlyRemainingCredits = item.MonthlyQuotaCredits - item.MonthlyUsageCredits
		if item.MonthlyRemainingCredits < 0 {
			item.MonthlyRemainingCredits = 0
		}
	}
	return nil
}

func resourceTypeForService(serviceID string) string {
	value := strings.ToLower(strings.TrimSpace(serviceID))
	switch {
	case strings.Contains(value, "code-agent") || strings.Contains(value, "code_agent") || strings.Contains(value, "codex"):
		return "code_agent"
	case strings.Contains(value, "aipod") || strings.Contains(value, "ai-pod"):
		return "aipod"
	case strings.Contains(value, "hwpod") || strings.Contains(value, "hw-pod"):
		return "hwpod"
	case value != "":
		return strings.Map(func(r rune) rune {
			if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' {
				return r
			}
			if r == '-' || r == '.' || r == '/' {
				return '_'
			}
			return -1
		}, value)
	default:
		return "generic"
	}
}

func parseJSONMap(raw string) map[string]any {
	result := map[string]any{}
	_ = json.Unmarshal([]byte(raw), &result)
	return result
}

func operatorFields(metadata map[string]any) (string, string, string) {
	operator, _ := metadata["operator"].(map[string]any)
	if operator == nil {
		return "", "", ""
	}
	return stringFromAny(operator["id"]), stringFromAny(operator["username"]), stringFromAny(operator["role"])
}

func stringFromAny(value any) string {
	if value == nil {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(value))
}

func (s *Server) principalForBilling(ctx context.Context, r *http.Request, token, apiKey, userID string) (*Principal, error) {
	if token == "" && apiKey == "" {
		token = bearerToken(r)
	}
	if token != "" || apiKey != "" {
		return s.principalFromToken(ctx, first(token, apiKey))
	}
	userID = strings.TrimSpace(userID)
	if userID == "" {
		return nil, errors.New("billing subject is required")
	}
	var principal Principal
	err := s.db.QueryRowContext(ctx, `SELECT id, email, username, role FROM hwlab_users WHERE id = $1 AND status = 'active'`, userID).Scan(&principal.UserID, &principal.Email, &principal.Username, &principal.Role)
	if err != nil {
		return nil, err
	}
	principal.AuthType = "internal-user-id"
	principal.Scopes = []string{"api"}
	return &principal, nil
}

func (s *Server) principalFromToken(ctx context.Context, token string) (*Principal, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return nil, errors.New("empty token")
	}
	hash := tokenHash(token)
	var principal Principal
	if strings.HasPrefix(token, "hwl_") {
		var scopesRaw string
		err := s.db.QueryRowContext(ctx, `SELECT u.id, u.email, u.username, u.role, k.id, k.scopes_json::text FROM hwlab_api_keys k JOIN hwlab_users u ON u.id = k.user_id WHERE k.key_hash = $1 AND k.status = 'active' AND k.revoked_at IS NULL AND u.status = 'active'`, hash).Scan(&principal.UserID, &principal.Email, &principal.Username, &principal.Role, &principal.KeyID, &scopesRaw)
		if err != nil {
			return nil, err
		}
		_ = json.Unmarshal([]byte(scopesRaw), &principal.Scopes)
		if len(principal.Scopes) == 0 {
			principal.Scopes = []string{"api"}
		}
		principal.AuthType = "api-key"
		_, _ = s.db.ExecContext(ctx, `UPDATE hwlab_api_keys SET last_used_at = now() WHERE id = $1`, principal.KeyID)
		return &principal, nil
	}
	err := s.db.QueryRowContext(ctx, `SELECT u.id, u.email, u.username, u.role FROM hwlab_user_sessions s JOIN hwlab_users u ON u.id = s.user_id WHERE s.token_hash = $1 AND s.revoked_at IS NULL AND s.expires_at > now() AND u.status = 'active'`, hash).Scan(&principal.UserID, &principal.Email, &principal.Username, &principal.Role)
	if err != nil {
		return nil, err
	}
	principal.AuthType = "session"
	principal.Scopes = []string{"session"}
	return &principal, nil
}

func (s *Server) requirePrincipal(w http.ResponseWriter, r *http.Request) (*Principal, bool) {
	if !s.databaseAvailable(w) {
		return nil, false
	}
	principal, err := s.principalFromToken(r.Context(), bearerToken(r))
	if err != nil {
		writeAPIError(w, http.StatusUnauthorized, "unauthorized", "valid bearer session or API key is required")
		return nil, false
	}
	return principal, true
}

func (s *Server) internalAllowed(w http.ResponseWriter, r *http.Request) bool {
	if s.config.InternalToken == "" {
		if s.config.RequireInternalToken {
			writeAPIError(w, http.StatusServiceUnavailable, "internal_token_missing", "internal token is required but not configured")
			return false
		}
		return true
	}
	candidate := strings.TrimSpace(r.Header.Get("X-HWLAB-Internal-Token"))
	if candidate == "" {
		candidate = bearerToken(r)
	}
	if subtle.ConstantTimeCompare([]byte(candidate), []byte(s.config.InternalToken)) != 1 {
		writeAPIError(w, http.StatusUnauthorized, "internal_auth_failed", "valid internal token is required")
		return false
	}
	return true
}

func (s *Server) databaseAvailable(w http.ResponseWriter) bool {
	if s.db != nil {
		return true
	}
	writeAPIError(w, http.StatusServiceUnavailable, "state_backend_not_configured", "HWLAB_USER_BILLING_DB_URL must point to the PK01 external PostgreSQL authority")
	return false
}

func (s *Server) registrationAvailable(w http.ResponseWriter) bool {
	if !s.RegistrationEnabled() {
		writeAPIError(w, http.StatusForbidden, "registration_disabled", "registration is disabled")
		return false
	}
	return s.databaseAvailable(w)
}

func (s *Server) RegistrationEnabled() bool {
	return s.config.RegistrationEnabled
}

func (s *Server) readyWriteProbe(ctx context.Context) error {
	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return err
	}
	probeID := newID("usr_probe")
	_, err = tx.ExecContext(ctx, `INSERT INTO hwlab_users (id, email, username, display_name, password_hash, status, role) VALUES ($1, $2, $3, 'readiness probe', 'readiness-probe', 'pending', 'user')`, probeID, probeID+"@readiness.invalid", probeID)
	if err != nil {
		_ = tx.Rollback()
		return err
	}
	return tx.Rollback()
}

func (s *Server) logDatabaseError(operation string, err error) {
	log.Printf("%s database operation=%s errorClass=%s errorCode=%s valuesRedacted=true", serviceID, operation, classifyDatabaseError(err), databaseErrorCode(err))
}

func classifyDatabaseError(err error) string {
	if err == nil {
		return "none"
	}
	text := strings.ToLower(err.Error())
	code := strings.ToUpper(databaseErrorCode(err))
	if code == "57014" || errors.Is(err, context.DeadlineExceeded) || strings.Contains(text, "deadline exceeded") || strings.Contains(text, "timeout") {
		return "timeout"
	}
	if strings.Contains(text, "certificate") || strings.Contains(text, "ssl") || strings.Contains(text, "tls") {
		return "ssl"
	}
	if strings.Contains(text, "password") || strings.Contains(text, "authentication") || strings.Contains(text, "pg_hba") || code == "28P01" || strings.HasPrefix(code, "28") {
		return "auth"
	}
	if strings.Contains(text, "too many connections") || strings.Contains(text, "connection refused") || strings.Contains(text, "connection reset") || strings.Contains(text, "broken pipe") {
		return "connection"
	}
	return "database"
}

func databaseErrorCode(err error) string {
	if err == nil {
		return ""
	}
	type sqlState interface{ SQLState() string }
	var state sqlState
	if errors.As(err, &state) {
		return state.SQLState()
	}
	type coder interface{ Code() string }
	var coded coder
	if errors.As(err, &coded) {
		return coded.Code()
	}
	return ""
}

func (s *Server) accountBalance(ctx context.Context, userID string) (int64, int64, error) {
	var balance, reserved int64
	err := s.db.QueryRowContext(ctx, `SELECT balance_credits, reserved_credits FROM hwlab_credit_accounts WHERE user_id = $1`, userID).Scan(&balance, &reserved)
	return balance, reserved, err
}

func (s *Server) accountPlanContext(ctx context.Context, userID string) (string, billingPlanSummary, []resourceEntitlementSummary, error) {
	planID := "default"
	if err := s.db.QueryRowContext(ctx, `SELECT COALESCE(plan_id, 'default') FROM hwlab_credit_accounts WHERE user_id = $1`, userID).Scan(&planID); err != nil {
		return "", billingPlanSummary{}, nil, err
	}
	plan, err := s.planByID(ctx, s.db, planID)
	if err != nil {
		return planID, billingPlanSummary{}, nil, err
	}
	entitlements, err := s.resourceEntitlements(ctx, s.db, planID, userID)
	return planID, plan, entitlements, err
}

func (s *Server) usageTotals(ctx context.Context, userID string) (int64, int64, int64, error) {
	var credits, quantity, count int64
	err := s.db.QueryRowContext(ctx, `SELECT COALESCE(SUM(credits), 0), COALESCE(SUM(quantity), 0), COUNT(*) FROM hwlab_usage_records WHERE user_id = $1`, userID).Scan(&credits, &quantity, &count)
	return credits, quantity, count, err
}

func (s *Server) usageByService(ctx context.Context, userID string, limit int) ([]serviceUsageSummary, error) {
	rows, err := s.db.QueryContext(ctx, `SELECT service_id, resource_type, COALESCE(SUM(credits), 0), COALESCE(SUM(quantity), 0), COUNT(*), MAX(created_at) FROM hwlab_usage_records WHERE user_id = $1 GROUP BY service_id, resource_type ORDER BY MAX(created_at) DESC NULLS LAST, service_id ASC LIMIT $2`, userID, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []serviceUsageSummary{}
	for rows.Next() {
		var item serviceUsageSummary
		if err := rows.Scan(&item.ServiceID, &item.ResourceType, &item.Credits, &item.Quantity, &item.RecordCount, &item.LastUsedAt); err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func (s *Server) ledgerRows(ctx context.Context, userID string, limit int) ([]ledgerSummaryRow, error) {
	rows, err := s.db.QueryContext(ctx, `SELECT id, kind, reason, delta_credits, balance_before, balance_after, source, status, COALESCE(idempotency_key, ''), COALESCE(operator_user_id, ''), COALESCE(operator_username, ''), metadata::text, created_at FROM hwlab_credit_ledger WHERE user_id = $1 ORDER BY created_at DESC, id DESC LIMIT $2`, userID, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []ledgerSummaryRow{}
	for rows.Next() {
		var item ledgerSummaryRow
		var before sql.NullInt64
		var metadataRaw string
		if err := rows.Scan(&item.ID, &item.Kind, &item.Reason, &item.DeltaCredits, &before, &item.BalanceAfter, &item.Source, &item.Status, &item.IdempotencyKey, &item.OperatorUserID, &item.OperatorUsername, &metadataRaw, &item.CreatedAt); err != nil {
			return nil, err
		}
		if before.Valid {
			value := before.Int64
			item.BalanceBefore = &value
		}
		item.Metadata = parseJSONMap(metadataRaw)
		items = append(items, item)
	}
	return items, rows.Err()
}

func (s *Server) activeReservations(ctx context.Context, userID string, limit int) ([]reservationSummaryRow, error) {
	rows, err := s.db.QueryContext(ctx, `SELECT id, service_id, resource_type, estimated_credits, estimated_tokens, status, expires_at, created_at FROM hwlab_billing_reservations WHERE user_id = $1 AND status = 'reserved' AND expires_at > now() ORDER BY expires_at ASC, created_at DESC LIMIT $2`, userID, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []reservationSummaryRow{}
	for rows.Next() {
		var item reservationSummaryRow
		if err := rows.Scan(&item.ReservationID, &item.ServiceID, &item.ResourceType, &item.EstimatedCredits, &item.EstimatedTokens, &item.Status, &item.ExpiresAt, &item.CreatedAt); err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func (s *Server) adminBillingUsers(ctx context.Context, limit int) ([]adminBillingUserSummary, error) {
	rows, err := s.db.QueryContext(ctx, `
SELECT
  u.id,
  u.email,
  u.username,
  u.display_name,
  u.status,
  u.role,
  u.email_verified,
  u.created_at,
  COALESCE(a.plan_id, 'default'),
  COALESCE(a.balance_credits, 0),
  COALESCE(a.reserved_credits, 0),
  COALESCE(usage.total_credits, 0),
  COALESCE(usage.total_quantity, 0),
  COALESCE(usage.record_count, 0),
  usage.last_used_at,
  COALESCE(keys.active_count, 0),
  COALESCE(keys.total_count, 0),
  keys.last_key_used_at,
  COALESCE(res.active_count, 0)
FROM hwlab_users u
LEFT JOIN hwlab_credit_accounts a ON a.user_id = u.id
LEFT JOIN (
  SELECT user_id, SUM(credits) AS total_credits, SUM(quantity) AS total_quantity, COUNT(*) AS record_count, MAX(created_at) AS last_used_at
  FROM hwlab_usage_records
  GROUP BY user_id
) usage ON usage.user_id = u.id
LEFT JOIN (
  SELECT user_id, COUNT(*) FILTER (WHERE status = 'active' AND revoked_at IS NULL) AS active_count, COUNT(*) AS total_count, MAX(last_used_at) AS last_key_used_at
  FROM hwlab_api_keys
  GROUP BY user_id
) keys ON keys.user_id = u.id
LEFT JOIN (
  SELECT user_id, COUNT(*) AS active_count
  FROM hwlab_billing_reservations
  WHERE status = 'reserved' AND expires_at > now()
  GROUP BY user_id
) res ON res.user_id = u.id
ORDER BY usage.last_used_at DESC NULLS LAST, u.created_at DESC, u.username ASC
LIMIT $1`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	users := []adminBillingUserSummary{}
	userIDs := []string{}
	for rows.Next() {
		var item adminBillingUserSummary
		var lastUsed sql.NullTime
		var lastKeyUsed sql.NullTime
		if err := rows.Scan(&item.User.ID, &item.User.Email, &item.User.Username, &item.User.DisplayName, &item.User.Status, &item.User.Role, &item.User.EmailVerified, &item.User.CreatedAt, &item.PlanID, &item.Credits.Balance, &item.Credits.Reserved, &item.Usage.TotalCredits, &item.Usage.TotalQuantity, &item.Usage.RecordCount, &lastUsed, &item.APIKeys.ActiveCount, &item.APIKeys.TotalCount, &lastKeyUsed, &item.Reservations.ActiveCount); err != nil {
			return nil, err
		}
		if lastUsed.Valid {
			value := lastUsed.Time
			item.Usage.LastUsedAt = &value
		}
		if lastKeyUsed.Valid {
			value := lastKeyUsed.Time
			item.APIKeys.LastUsedAt = &value
		}
		item.Credits.Available = item.Credits.Balance - item.Credits.Reserved
		userIDs = append(userIDs, item.User.ID)
		users = append(users, item)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	reservations, err := s.activeReservationsForUsers(ctx, userIDs, 5)
	if err != nil {
		return nil, err
	}
	for i := range users {
		users[i].Reservations.Active = reservations[users[i].User.ID]
	}
	if err := s.enrichAdminBillingUsers(ctx, users); err != nil {
		return nil, err
	}
	return users, nil
}

func (s *Server) activeReservationsForUsers(ctx context.Context, userIDs []string, perUserLimit int) (map[string][]reservationSummaryRow, error) {
	items := map[string][]reservationSummaryRow{}
	if len(userIDs) == 0 {
		return items, nil
	}
	placeholders := make([]string, len(userIDs))
	args := make([]any, 0, len(userIDs)+1)
	for i, userID := range userIDs {
		placeholders[i] = "$" + strconv.Itoa(i+1)
		args = append(args, userID)
	}
	limitParam := "$" + strconv.Itoa(len(args)+1)
	args = append(args, perUserLimit)
	query := `
SELECT user_id, id, service_id, resource_type, estimated_credits, estimated_tokens, status, expires_at, created_at
FROM (
  SELECT user_id, id, service_id, resource_type, estimated_credits, estimated_tokens, status, expires_at, created_at,
         row_number() OVER (PARTITION BY user_id ORDER BY expires_at ASC, created_at DESC) AS rn
  FROM hwlab_billing_reservations
  WHERE user_id IN (` + strings.Join(placeholders, ", ") + `) AND status = 'reserved' AND expires_at > now()
) ranked
WHERE rn <= ` + limitParam + `
ORDER BY expires_at ASC, created_at DESC`
	rows, err := s.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	for rows.Next() {
		var userID string
		var item reservationSummaryRow
		if err := rows.Scan(&userID, &item.ReservationID, &item.ServiceID, &item.ResourceType, &item.EstimatedCredits, &item.EstimatedTokens, &item.Status, &item.ExpiresAt, &item.CreatedAt); err != nil {
			return nil, err
		}
		items[userID] = append(items[userID], item)
	}
	return items, rows.Err()
}

func (s *Server) adminBillingTotals(ctx context.Context) (map[string]any, error) {
	var totals struct {
		Users              int64
		ActiveUsers        int64
		DisabledUsers      int64
		PendingUsers       int64
		BalanceCredits     int64
		ReservedCredits    int64
		AvailableCredits   int64
		UsageCredits       int64
		UsageQuantity      int64
		UsageRecords       int64
		ActiveReservations int64
	}
	err := s.db.QueryRowContext(ctx, `
SELECT
  (SELECT COUNT(*) FROM hwlab_users),
  (SELECT COUNT(*) FROM hwlab_users WHERE status = 'active'),
  (SELECT COUNT(*) FROM hwlab_users WHERE status = 'disabled'),
  (SELECT COUNT(*) FROM hwlab_users WHERE status = 'pending'),
  COALESCE((SELECT SUM(balance_credits) FROM hwlab_credit_accounts), 0),
  COALESCE((SELECT SUM(reserved_credits) FROM hwlab_credit_accounts), 0),
  COALESCE((SELECT SUM(balance_credits - reserved_credits) FROM hwlab_credit_accounts), 0),
  COALESCE((SELECT SUM(credits) FROM hwlab_usage_records), 0),
  COALESCE((SELECT SUM(quantity) FROM hwlab_usage_records), 0),
  (SELECT COUNT(*) FROM hwlab_usage_records),
  (SELECT COUNT(*) FROM hwlab_billing_reservations WHERE status = 'reserved' AND expires_at > now())`).Scan(&totals.Users, &totals.ActiveUsers, &totals.DisabledUsers, &totals.PendingUsers, &totals.BalanceCredits, &totals.ReservedCredits, &totals.AvailableCredits, &totals.UsageCredits, &totals.UsageQuantity, &totals.UsageRecords, &totals.ActiveReservations)
	if err != nil {
		return nil, err
	}
	return map[string]any{
		"users":              totals.Users,
		"activeUsers":        totals.ActiveUsers,
		"disabledUsers":      totals.DisabledUsers,
		"pendingUsers":       totals.PendingUsers,
		"balanceCredits":     totals.BalanceCredits,
		"reservedCredits":    totals.ReservedCredits,
		"availableCredits":   totals.AvailableCredits,
		"usageCredits":       totals.UsageCredits,
		"usageQuantity":      totals.UsageQuantity,
		"usageRecords":       totals.UsageRecords,
		"activeReservations": totals.ActiveReservations,
	}, nil
}

func (s *Server) apiKeysForUser(ctx context.Context, userID string) ([]apiKeySummary, error) {
	rows, err := s.db.QueryContext(ctx, `SELECT id, name, key_prefix, scopes_json::text, status, created_at, last_used_at, revoked_at FROM hwlab_api_keys WHERE user_id = $1 ORDER BY created_at DESC, id DESC`, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	keys := []apiKeySummary{}
	for rows.Next() {
		key, err := scanAPIKey(rows)
		if err != nil {
			return nil, err
		}
		keys = append(keys, key)
	}
	return keys, rows.Err()
}

func (s *Server) apiKeyForUser(ctx context.Context, userID, keyID string) (apiKeySummary, error) {
	return scanAPIKey(s.db.QueryRowContext(ctx, `SELECT id, name, key_prefix, scopes_json::text, status, created_at, last_used_at, revoked_at FROM hwlab_api_keys WHERE user_id = $1 AND id = $2`, userID, keyID))
}

type apiKeyScanner interface {
	Scan(dest ...any) error
}

func scanAPIKey(scanner apiKeyScanner) (apiKeySummary, error) {
	var key apiKeySummary
	var scopesRaw string
	var lastUsed sql.NullTime
	var revoked sql.NullTime
	if err := scanner.Scan(&key.ID, &key.Name, &key.Prefix, &scopesRaw, &key.Status, &key.CreatedAt, &lastUsed, &revoked); err != nil {
		return key, err
	}
	_ = json.Unmarshal([]byte(scopesRaw), &key.Scopes)
	if len(key.Scopes) == 0 {
		key.Scopes = []string{"api"}
	}
	if lastUsed.Valid {
		value := lastUsed.Time
		key.LastUsedAt = &value
	}
	if revoked.Valid {
		value := revoked.Time
		key.RevokedAt = &value
	}
	return key, nil
}

func adminUsersQueryFromRequest(r *http.Request) adminUsersQuery {
	page := queryLimit(r, "page", 1, 100000)
	pageSize := queryLimit(r, "pageSize", queryLimit(r, "limit", 50, 100), 100)
	status := normalizeUserStatus(r.URL.Query().Get("status"), "")
	role := normalizeRole(r.URL.Query().Get("role"), "")
	return adminUsersQuery{
		Page:     page,
		PageSize: pageSize,
		Search:   strings.TrimSpace(r.URL.Query().Get("search")),
		Status:   status,
		Role:     role,
	}
}

func (s *Server) adminUsers(ctx context.Context, query adminUsersQuery) ([]adminBillingUserSummary, int64, error) {
	where, args := adminUsersWhere(query)
	var total int64
	countQuery := `SELECT COUNT(*) FROM hwlab_users u ` + where
	if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	offset := (query.Page - 1) * query.PageSize
	args = append(args, query.PageSize, offset)
	limitParam := "$" + strconv.Itoa(len(args)-1)
	offsetParam := "$" + strconv.Itoa(len(args))
	rows, err := s.db.QueryContext(ctx, `
SELECT
  u.id,
  u.email,
  u.username,
  u.display_name,
  u.status,
  u.role,
  u.email_verified,
  u.created_at,
  COALESCE(a.plan_id, 'default'),
  COALESCE(a.balance_credits, 0),
  COALESCE(a.reserved_credits, 0),
  COALESCE(usage.total_credits, 0),
  COALESCE(usage.total_quantity, 0),
  COALESCE(usage.record_count, 0),
  usage.last_used_at,
  COALESCE(keys.active_count, 0),
  COALESCE(keys.total_count, 0),
  keys.last_key_used_at,
  COALESCE(res.active_count, 0)
FROM hwlab_users u
LEFT JOIN hwlab_credit_accounts a ON a.user_id = u.id
LEFT JOIN (
  SELECT user_id, SUM(credits) AS total_credits, SUM(quantity) AS total_quantity, COUNT(*) AS record_count, MAX(created_at) AS last_used_at
  FROM hwlab_usage_records
  GROUP BY user_id
) usage ON usage.user_id = u.id
LEFT JOIN (
  SELECT user_id, COUNT(*) FILTER (WHERE status = 'active' AND revoked_at IS NULL) AS active_count, COUNT(*) AS total_count, MAX(last_used_at) AS last_key_used_at
  FROM hwlab_api_keys
  GROUP BY user_id
) keys ON keys.user_id = u.id
LEFT JOIN (
  SELECT user_id, COUNT(*) AS active_count
  FROM hwlab_billing_reservations
  WHERE status = 'reserved' AND expires_at > now()
  GROUP BY user_id
) res ON res.user_id = u.id
`+where+`
ORDER BY usage.last_used_at DESC NULLS LAST, u.created_at DESC, u.username ASC
LIMIT `+limitParam+` OFFSET `+offsetParam, args...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	users, userIDs, err := scanAdminBillingUsers(rows)
	if err != nil {
		return nil, 0, err
	}
	reservations, err := s.activeReservationsForUsers(ctx, userIDs, 5)
	if err != nil {
		return nil, 0, err
	}
	for i := range users {
		users[i].Reservations.Active = reservations[users[i].User.ID]
	}
	if err := s.enrichAdminBillingUsers(ctx, users); err != nil {
		return nil, 0, err
	}
	return users, total, nil
}

func (s *Server) enrichAdminBillingUsers(ctx context.Context, users []adminBillingUserSummary) error {
	for i := range users {
		plan, err := s.planByID(ctx, s.db, users[i].PlanID)
		if err != nil {
			return err
		}
		entitlements, err := s.resourceEntitlements(ctx, s.db, users[i].PlanID, users[i].User.ID)
		if err != nil {
			return err
		}
		users[i].Plan = plan
		users[i].Entitlements = entitlements
	}
	return nil
}

func adminUsersWhere(query adminUsersQuery) (string, []any) {
	clauses := []string{}
	args := []any{}
	if query.Status != "" {
		args = append(args, query.Status)
		clauses = append(clauses, "u.status = $"+strconv.Itoa(len(args)))
	}
	if query.Role != "" {
		args = append(args, query.Role)
		clauses = append(clauses, "u.role = $"+strconv.Itoa(len(args)))
	}
	if query.Search != "" {
		args = append(args, "%"+strings.ToLower(query.Search)+"%")
		param := "$" + strconv.Itoa(len(args))
		clauses = append(clauses, "(lower(u.id) LIKE "+param+" OR lower(u.email) LIKE "+param+" OR lower(u.username) LIKE "+param+" OR lower(u.display_name) LIKE "+param+")")
	}
	if len(clauses) == 0 {
		return "", args
	}
	return "WHERE " + strings.Join(clauses, " AND "), args
}

func (s *Server) adminUserDetail(ctx context.Context, userID string) (adminUserDetail, error) {
	users, _, err := s.adminUsers(ctx, adminUsersQuery{Page: 1, PageSize: 1, Search: userID})
	if err != nil {
		return adminUserDetail{}, err
	}
	var summary adminBillingUserSummary
	found := false
	for _, user := range users {
		if user.User.ID == userID {
			summary = user
			found = true
			break
		}
	}
	if !found {
		return adminUserDetail{}, sql.ErrNoRows
	}
	ledger, err := s.ledgerRows(ctx, userID, 50)
	if err != nil {
		return adminUserDetail{}, err
	}
	keys, err := s.apiKeysForUser(ctx, userID)
	if err != nil {
		return adminUserDetail{}, err
	}
	usageBy, err := s.usageByService(ctx, userID, 50)
	if err != nil {
		return adminUserDetail{}, err
	}
	reservations, err := s.activeReservations(ctx, userID, 50)
	if err != nil {
		return adminUserDetail{}, err
	}
	return adminUserDetail{Summary: summary, Ledger: ledger, APIKeys: keys, UsageBy: usageBy, Reservations: reservations}, nil
}

func scanAdminBillingUsers(rows *sql.Rows) ([]adminBillingUserSummary, []string, error) {
	users := []adminBillingUserSummary{}
	userIDs := []string{}
	for rows.Next() {
		var item adminBillingUserSummary
		var lastUsed sql.NullTime
		var lastKeyUsed sql.NullTime
		if err := rows.Scan(&item.User.ID, &item.User.Email, &item.User.Username, &item.User.DisplayName, &item.User.Status, &item.User.Role, &item.User.EmailVerified, &item.User.CreatedAt, &item.PlanID, &item.Credits.Balance, &item.Credits.Reserved, &item.Usage.TotalCredits, &item.Usage.TotalQuantity, &item.Usage.RecordCount, &lastUsed, &item.APIKeys.ActiveCount, &item.APIKeys.TotalCount, &lastKeyUsed, &item.Reservations.ActiveCount); err != nil {
			return nil, nil, err
		}
		if lastUsed.Valid {
			value := lastUsed.Time
			item.Usage.LastUsedAt = &value
		}
		if lastKeyUsed.Valid {
			value := lastKeyUsed.Time
			item.APIKeys.LastUsedAt = &value
		}
		item.Credits.Available = item.Credits.Balance - item.Credits.Reserved
		userIDs = append(userIDs, item.User.ID)
		users = append(users, item)
	}
	if err := rows.Err(); err != nil {
		return nil, nil, err
	}
	return users, userIDs, nil
}

func normalizeRole(value, fallback string) string {
	role := strings.TrimSpace(strings.ToLower(value))
	if role == "" {
		return fallback
	}
	if role == "user" || role == "admin" {
		return role
	}
	return ""
}

func normalizeUserStatus(value, fallback string) string {
	status := strings.TrimSpace(strings.ToLower(value))
	if status == "" {
		return fallback
	}
	if status == "active" || status == "disabled" || status == "pending" {
		return status
	}
	return ""
}

func queryLimit(r *http.Request, name string, fallback, max int) int {
	value := strings.TrimSpace(r.URL.Query().Get(name))
	if value == "" {
		return fallback
	}
	parsed, err := strconv.Atoi(value)
	if err != nil || parsed <= 0 {
		return fallback
	}
	if parsed > max {
		return max
	}
	return parsed
}

func (s *Server) estimateCredits(explicit, tokens int64) int64 {
	if explicit > 0 {
		return explicit
	}
	if tokens <= 0 {
		return 1
	}
	per := s.config.CreditPerThousandTok
	if per <= 0 {
		per = 1
	}
	return int64(math.Ceil(float64(tokens*per) / 1000.0))
}

func decodeJSON(w http.ResponseWriter, r *http.Request, target any) bool {
	defer r.Body.Close()
	decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		writeAPIError(w, http.StatusBadRequest, "invalid_json", "request body must be valid JSON")
		return false
	}
	return true
}

func writeAPIError(w http.ResponseWriter, status int, code, message string) {
	writeJSON(w, status, map[string]any{"error": apiError{Code: code, Message: message}})
}

func writeJSON(w http.ResponseWriter, status int, payload any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(payload)
}

func bearerToken(r *http.Request) string {
	value := strings.TrimSpace(r.Header.Get("Authorization"))
	if strings.HasPrefix(strings.ToLower(value), "bearer ") {
		return strings.TrimSpace(value[7:])
	}
	return ""
}

func normalizeUsername(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	value = strings.Map(func(r rune) rune {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
			return r
		}
		return -1
	}, value)
	if len(value) < 3 || len(value) > 64 {
		return ""
	}
	return value
}

func hashPassword(password string) (string, error) {
	salt, err := randomBytes(16)
	if err != nil {
		return "", err
	}
	iterations := 210000
	digest := pbkdf2SHA256([]byte(password), salt, iterations, 32)
	return fmt.Sprintf("pbkdf2_sha256$%d$%s$%s", iterations, b64(salt), b64(digest)), nil
}

func verifyPassword(encoded, password string) bool {
	if strings.HasPrefix(encoded, "sha256:") {
		parts := strings.Split(encoded, ":")
		if len(parts) != 3 || parts[1] == "" || parts[2] == "" {
			return false
		}
		digest := sha256.Sum256([]byte(parts[1] + ":" + password))
		actual := hex.EncodeToString(digest[:])
		return subtle.ConstantTimeCompare([]byte(actual), []byte(parts[2])) == 1
	}
	parts := strings.Split(encoded, "$")
	if len(parts) != 4 || parts[0] != "pbkdf2_sha256" {
		return false
	}
	iterations, err := strconv.Atoi(parts[1])
	if err != nil || iterations <= 0 {
		return false
	}
	salt, err := base64.RawURLEncoding.DecodeString(parts[2])
	if err != nil {
		return false
	}
	expected, err := base64.RawURLEncoding.DecodeString(parts[3])
	if err != nil {
		return false
	}
	actual := pbkdf2SHA256([]byte(password), salt, iterations, len(expected))
	return subtle.ConstantTimeCompare(actual, expected) == 1
}

func pbkdf2SHA256(password, salt []byte, iterations, keyLen int) []byte {
	hLen := sha256.Size
	numBlocks := (keyLen + hLen - 1) / hLen
	var output []byte
	for block := 1; block <= numBlocks; block++ {
		u := pbkdf2Block(password, salt, block)
		t := append([]byte(nil), u...)
		for i := 1; i < iterations; i++ {
			u = hmacSHA256(password, u)
			for j := range t {
				t[j] ^= u[j]
			}
		}
		output = append(output, t...)
	}
	return output[:keyLen]
}

func pbkdf2Block(password, salt []byte, block int) []byte {
	buf := make([]byte, len(salt)+4)
	copy(buf, salt)
	binary.BigEndian.PutUint32(buf[len(salt):], uint32(block))
	return hmacSHA256(password, buf)
}

func hmacSHA256(key, data []byte) []byte {
	h := hmac.New(sha256.New, key)
	_, _ = h.Write(data)
	return h.Sum(nil)
}

func tokenHash(token string) string {
	sum := sha256.Sum256([]byte(token))
	return hex.EncodeToString(sum[:])
}

func newID(prefix string) string {
	return prefix + "_" + randomToken(16)
}

func randomToken(size int) string {
	buf, err := randomBytes(size)
	if err != nil {
		panic(err)
	}
	return b64(buf)
}

func randomBytes(size int) ([]byte, error) {
	buf := make([]byte, size)
	_, err := rand.Read(buf)
	return buf, err
}

func b64(value []byte) string {
	return base64.RawURLEncoding.EncodeToString(value)
}

func jsonObject(value map[string]any) string {
	if value == nil {
		return "{}"
	}
	encoded, err := json.Marshal(value)
	if err != nil {
		return "{}"
	}
	return string(encoded)
}

func parseInt64(value string, fallback int64) int64 {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	parsed, err := strconv.ParseInt(value, 10, 64)
	if err != nil {
		return fallback
	}
	return parsed
}

func envInt(name string, fallback int) int {
	value := strings.TrimSpace(os.Getenv(name))
	if value == "" {
		return fallback
	}
	parsed, err := strconv.Atoi(value)
	if err != nil || parsed <= 0 {
		return fallback
	}
	return parsed
}

func parseDurationHours(value string, fallbackHours int64) time.Duration {
	return time.Duration(parseInt64(value, fallbackHours)) * time.Hour
}

func parseDurationMinutes(value string, fallbackMinutes int64) time.Duration {
	return time.Duration(parseInt64(value, fallbackMinutes)) * time.Minute
}

func first(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

func nonNegative(value int64) int64 {
	if value < 0 {
		return 0
	}
	return value
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func isDuplicate(err error) bool {
	return err != nil && strings.Contains(strings.ToLower(err.Error()), "duplicate")
}
