package app

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"database/sql"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"golang.org/x/crypto/bcrypt"

	"lingniu/vehicle-data-platform/apps/api/internal/config"
	"lingniu/vehicle-data-platform/apps/api/internal/httpx"
	"lingniu/vehicle-data-platform/apps/api/internal/platform"
)

const (
	localSessionCacheTTL = 30 * time.Second
	loginLockDuration    = 15 * time.Minute
	maxLoginFailures     = 5
)

var usernamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{2,63}$`)

var customerMenuSet = map[string]bool{
	"monitor": true, "vehicles": true, "tracks": true, "statistics": true,
}

var adminMenus = []string{"monitor", "vehicles", "tracks", "history", "statistics", "alerts", "access", "operations", "users"}

type authUser struct {
	ID              uint64                    `json:"id"`
	Username        string                    `json:"username"`
	DisplayName     string                    `json:"displayName"`
	UserType        string                    `json:"userType"`
	Status          string                    `json:"status"`
	CustomerRef     string                    `json:"customerRef"`
	TenantRef       string                    `json:"tenantRef"`
	AuthProvider    string                    `json:"authProvider"`
	ExternalSubject string                    `json:"externalSubject,omitempty"`
	MenuKeys        []string                  `json:"menuKeys"`
	VehicleVINs     []string                  `json:"vehicleVins"`
	Vehicles        []authVehicleGrant        `json:"vehicles"`
	GrantHistory    []authVehicleGrantHistory `json:"grantHistory"`
	LastLoginAt     *time.Time                `json:"lastLoginAt,omitempty"`
	CreatedAt       time.Time                 `json:"createdAt"`
	UpdatedAt       time.Time                 `json:"updatedAt"`
}

type authVehicleGrant struct {
	VIN          string     `json:"vin"`
	Plate        string     `json:"plate"`
	ValidFrom    time.Time  `json:"validFrom"`
	ValidTo      *time.Time `json:"validTo,omitempty"`
	SourceSystem string     `json:"sourceSystem"`
	GrantedBy    string     `json:"grantedBy"`
}

type authVehicleGrantHistory struct {
	ID           uint64     `json:"id"`
	VIN          string     `json:"vin"`
	Plate        string     `json:"plate"`
	ValidFrom    time.Time  `json:"validFrom"`
	ValidTo      *time.Time `json:"validTo,omitempty"`
	SourceSystem string     `json:"sourceSystem"`
	GrantedBy    string     `json:"grantedBy"`
	RevokedBy    string     `json:"revokedBy"`
	CreatedAt    time.Time  `json:"createdAt"`
	UpdatedAt    time.Time  `json:"updatedAt"`
}

type userVehicleGrantInput struct {
	VIN       string `json:"vin"`
	ValidFrom string `json:"validFrom"`
	ValidTo   string `json:"validTo"`
}

type vehicleGrantMutation struct {
	VIN       string
	ValidFrom *time.Time
	ValidTo   *time.Time
}

type vehicleGrantAuditSnapshot struct {
	ValidFrom time.Time  `json:"validFrom"`
	ValidTo   *time.Time `json:"validTo,omitempty"`
}

type vehicleGrantAuditChange struct {
	VIN    string                     `json:"vin"`
	Before *vehicleGrantAuditSnapshot `json:"before,omitempty"`
	After  *vehicleGrantAuditSnapshot `json:"after,omitempty"`
}

type localCredential struct {
	authUser
	PasswordHash     string
	FailedLoginCount int
	LockedUntil      sql.NullTime
}

type userMutation struct {
	Username      string                  `json:"username"`
	DisplayName   string                  `json:"displayName"`
	Password      string                  `json:"password"`
	Status        string                  `json:"status"`
	CustomerRef   string                  `json:"customerRef"`
	TenantRef     string                  `json:"tenantRef"`
	MenuKeys      []string                `json:"menuKeys"`
	VehicleVINs   []string                `json:"vehicleVins"`
	VehicleGrants []userVehicleGrantInput `json:"vehicleGrants"`
}

type loginRequest struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type passwordChangeRequest struct {
	CurrentPassword string `json:"currentPassword"`
	NewPassword     string `json:"newPassword"`
}

type loginResponse struct {
	AccessToken string             `json:"accessToken"`
	ExpiresAt   time.Time          `json:"expiresAt"`
	Session     platform.Principal `json:"session"`
}

type cachedSession struct {
	principal platform.Principal
	expiresAt time.Time
	cachedAt  time.Time
}

type authStore struct {
	db         *sql.DB
	sessionTTL time.Duration
	cacheMu    sync.RWMutex
	cache      map[string]cachedSession
}

func newAuthStore(db *sql.DB, cfg config.Config) (*authStore, error) {
	if db == nil {
		return nil, nil
	}
	ttl := cfg.SessionTTL
	if ttl <= 0 {
		ttl = 12 * time.Hour
	}
	store := &authStore{db: db, sessionTTL: ttl, cache: map[string]cachedSession{}}
	if err := store.ensureBootstrapAdmin(context.Background(), cfg.BootstrapAdminUsername, cfg.BootstrapAdminPassword); err != nil {
		return nil, err
	}
	return store, nil
}

func (s *authStore) ensureBootstrapAdmin(ctx context.Context, username, password string) error {
	username = strings.TrimSpace(username)
	if username == "" && password == "" {
		return nil
	}
	if !usernamePattern.MatchString(username) {
		return fmt.Errorf("BOOTSTRAP_ADMIN_USERNAME is invalid")
	}
	if err := validatePassword(password); err != nil {
		return fmt.Errorf("BOOTSTRAP_ADMIN_PASSWORD: %w", err)
	}
	var exists int
	if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM platform_user WHERE user_type='admin'`).Scan(&exists); err != nil {
		return fmt.Errorf("check bootstrap admin: %w", err)
	}
	if exists > 0 {
		return nil
	}
	hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
	if err != nil {
		return fmt.Errorf("hash bootstrap password: %w", err)
	}
	_, err = s.db.ExecContext(ctx, `INSERT INTO platform_user(username,display_name,password_hash,user_type,status,auth_provider,created_by,updated_by) VALUES(?,?,?,'admin','enabled','local','bootstrap','bootstrap')`, username, "平台管理员", string(hash))
	if err != nil {
		return fmt.Errorf("create bootstrap admin: %w", err)
	}
	return nil
}

func (s *authStore) login(w http.ResponseWriter, r *http.Request) {
	var input loginRequest
	if !decodeAuthJSON(w, r, &input) {
		return
	}
	input.Username = strings.TrimSpace(input.Username)
	if !usernamePattern.MatchString(input.Username) || len(input.Password) > 128 {
		s.audit(r.Context(), input.Username, "login", "user", input.Username, "denied", map[string]any{"reason": "invalid_credentials"}, remoteAddress(r))
		httpx.WriteError(w, http.StatusUnauthorized, "LOGIN_FAILED", "用户名或密码错误", "", requestTraceID(r))
		return
	}
	credential, err := s.localCredential(r.Context(), input.Username)
	if err != nil {
		if !errors.Is(err, sql.ErrNoRows) {
			httpx.WriteError(w, http.StatusInternalServerError, "AUTH_STORE_FAILED", "登录服务暂时不可用", "", requestTraceID(r))
			return
		}
		s.audit(r.Context(), input.Username, "login", "user", input.Username, "denied", map[string]any{"reason": "invalid_credentials"}, remoteAddress(r))
		httpx.WriteError(w, http.StatusUnauthorized, "LOGIN_FAILED", "用户名或密码错误", "", requestTraceID(r))
		return
	}
	now := time.Now()
	if credential.Status != "enabled" {
		s.audit(r.Context(), input.Username, "login", "user", strconv.FormatUint(credential.ID, 10), "denied", map[string]any{"reason": "disabled"}, remoteAddress(r))
		httpx.WriteError(w, http.StatusForbidden, "ACCOUNT_DISABLED", "账号已停用，请联系管理员", "", requestTraceID(r))
		return
	}
	if credential.LockedUntil.Valid && credential.LockedUntil.Time.After(now) {
		httpx.WriteError(w, http.StatusTooManyRequests, "ACCOUNT_LOCKED", "登录失败次数过多，请稍后再试", credential.LockedUntil.Time.Format(time.RFC3339), requestTraceID(r))
		return
	}
	if bcrypt.CompareHashAndPassword([]byte(credential.PasswordHash), []byte(input.Password)) != nil {
		failures := credential.FailedLoginCount + 1
		var locked any
		if failures >= maxLoginFailures {
			locked = now.Add(loginLockDuration)
		}
		_, _ = s.db.ExecContext(r.Context(), `UPDATE platform_user SET failed_login_count=?,locked_until=? WHERE id=?`, failures, locked, credential.ID)
		s.audit(r.Context(), input.Username, "login", "user", strconv.FormatUint(credential.ID, 10), "denied", map[string]any{"reason": "invalid_credentials", "failures": failures}, remoteAddress(r))
		httpx.WriteError(w, http.StatusUnauthorized, "LOGIN_FAILED", "用户名或密码错误", "", requestTraceID(r))
		return
	}
	principal, err := s.principalForUser(r.Context(), credential.authUser)
	if err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "AUTH_STORE_FAILED", "登录服务暂时不可用", "", requestTraceID(r))
		return
	}
	accessToken, tokenHash, err := randomSessionToken()
	if err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "SESSION_CREATE_FAILED", "无法创建登录会话", "", requestTraceID(r))
		return
	}
	sessionID, err := randomHex(16)
	if err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "SESSION_CREATE_FAILED", "无法创建登录会话", "", requestTraceID(r))
		return
	}
	expiresAt := now.Add(s.sessionTTL)
	_, err = s.db.ExecContext(r.Context(), `INSERT INTO platform_user_session(id,user_id,token_hash,issued_at,expires_at,last_seen_at,remote_addr,user_agent) VALUES(?,?,?,?,?,?,?,?)`, sessionID, credential.ID, tokenHash[:], now, expiresAt, now, remoteAddress(r), truncateUTF8(r.UserAgent(), 255))
	if err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "SESSION_CREATE_FAILED", "无法创建登录会话", "", requestTraceID(r))
		return
	}
	_, _ = s.db.ExecContext(r.Context(), `UPDATE platform_user SET failed_login_count=0,locked_until=NULL,last_login_at=? WHERE id=?`, now, credential.ID)
	principal.SessionID = sessionID
	s.cachePut(hex.EncodeToString(tokenHash[:]), principal, expiresAt)
	s.audit(r.Context(), principal.Name, "login", "user", strconv.FormatUint(credential.ID, 10), "success", nil, remoteAddress(r))
	w.Header().Set("Cache-Control", "no-store")
	httpx.WriteOK(w, requestTraceID(r), loginResponse{AccessToken: accessToken, ExpiresAt: expiresAt, Session: principal})
}

func (s *authStore) authenticate(ctx context.Context, token string) (platform.Principal, bool) {
	hash := sha256.Sum256([]byte(token))
	key := hex.EncodeToString(hash[:])
	if principal, ok := s.cacheGet(key); ok {
		return principal, true
	}
	var user authUser
	var sessionID string
	var expiresAt time.Time
	var external sql.NullString
	err := s.db.QueryRowContext(ctx, `SELECT u.id,u.username,u.display_name,u.user_type,u.status,u.customer_ref,u.tenant_ref,u.auth_provider,u.external_subject,u.last_login_at,u.created_at,u.updated_at,s.id,s.expires_at FROM platform_user_session s JOIN platform_user u ON u.id=s.user_id WHERE s.token_hash=? AND s.revoked_at IS NULL AND s.expires_at>NOW(3) AND u.status='enabled'`, hash[:]).Scan(
		&user.ID, &user.Username, &user.DisplayName, &user.UserType, &user.Status, &user.CustomerRef, &user.TenantRef, &user.AuthProvider, &external, &user.LastLoginAt, &user.CreatedAt, &user.UpdatedAt, &sessionID, &expiresAt,
	)
	if err != nil {
		return platform.Principal{}, false
	}
	user.ExternalSubject = external.String
	principal, err := s.principalForUser(ctx, user)
	if err != nil {
		return platform.Principal{}, false
	}
	principal.SessionID = sessionID
	s.cachePut(key, principal, expiresAt)
	return principal, true
}

func (s *authStore) logout(ctx context.Context, token string) {
	hash := sha256.Sum256([]byte(token))
	_, _ = s.db.ExecContext(ctx, `UPDATE platform_user_session SET revoked_at=NOW(3) WHERE token_hash=? AND revoked_at IS NULL`, hash[:])
	s.cacheMu.Lock()
	delete(s.cache, hex.EncodeToString(hash[:]))
	s.cacheMu.Unlock()
}

func (s *authStore) changePassword(w http.ResponseWriter, r *http.Request, principal platform.Principal) {
	if principal.AuthProvider != "local" || principal.SubjectID == "" {
		httpx.WriteError(w, http.StatusBadRequest, "PASSWORD_CHANGE_UNAVAILABLE", "当前登录方式不支持修改密码", "", requestTraceID(r))
		return
	}
	var input passwordChangeRequest
	if !decodeAuthJSON(w, r, &input) {
		return
	}
	if err := validatePassword(input.NewPassword); err != nil {
		httpx.WriteError(w, http.StatusBadRequest, "PASSWORD_INVALID", err.Error(), "", requestTraceID(r))
		return
	}
	id, _ := strconv.ParseUint(principal.SubjectID, 10, 64)
	var currentHash string
	if err := s.db.QueryRowContext(r.Context(), `SELECT password_hash FROM platform_user WHERE id=? AND status='enabled'`, id).Scan(&currentHash); err != nil || bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(input.CurrentPassword)) != nil {
		s.audit(r.Context(), principal.Name, "password.change", "user", principal.SubjectID, "denied", map[string]any{"reason": "current_password_invalid"}, remoteAddress(r))
		httpx.WriteError(w, http.StatusUnauthorized, "CURRENT_PASSWORD_INVALID", "当前密码不正确", "", requestTraceID(r))
		return
	}
	if bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(input.NewPassword)) == nil {
		httpx.WriteError(w, http.StatusBadRequest, "PASSWORD_UNCHANGED", "新密码不能与当前密码相同", "", requestTraceID(r))
		return
	}
	hash, _ := bcrypt.GenerateFromPassword([]byte(input.NewPassword), 12)
	tx, err := s.db.BeginTx(r.Context(), nil)
	if err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "PASSWORD_CHANGE_FAILED", "无法修改密码", "", requestTraceID(r))
		return
	}
	defer tx.Rollback()
	if _, err := tx.ExecContext(r.Context(), `UPDATE platform_user SET password_hash=?,password_changed_at=NOW(3),failed_login_count=0,locked_until=NULL,updated_by=? WHERE id=?`, string(hash), principal.Name, id); err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "PASSWORD_CHANGE_FAILED", "无法修改密码", "", requestTraceID(r))
		return
	}
	if _, err := tx.ExecContext(r.Context(), `UPDATE platform_user_session SET revoked_at=NOW(3) WHERE user_id=? AND revoked_at IS NULL`, id); err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "PASSWORD_CHANGE_FAILED", "无法撤销旧会话", "", requestTraceID(r))
		return
	}
	if err := tx.Commit(); err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "PASSWORD_CHANGE_FAILED", "无法修改密码", "", requestTraceID(r))
		return
	}
	s.invalidateUser(id)
	s.audit(r.Context(), principal.Name, "password.change", "user", principal.SubjectID, "success", nil, remoteAddress(r))
	httpx.WriteOK(w, requestTraceID(r), map[string]bool{"changed": true})
}

func (s *authStore) localCredential(ctx context.Context, username string) (localCredential, error) {
	var credential localCredential
	var external sql.NullString
	err := s.db.QueryRowContext(ctx, `SELECT id,username,display_name,password_hash,user_type,status,customer_ref,tenant_ref,auth_provider,external_subject,failed_login_count,locked_until,last_login_at,created_at,updated_at FROM platform_user WHERE username=? AND auth_provider='local'`, username).Scan(
		&credential.ID, &credential.Username, &credential.DisplayName, &credential.PasswordHash, &credential.UserType, &credential.Status, &credential.CustomerRef, &credential.TenantRef, &credential.AuthProvider, &external, &credential.FailedLoginCount, &credential.LockedUntil, &credential.LastLoginAt, &credential.CreatedAt, &credential.UpdatedAt,
	)
	credential.ExternalSubject = external.String
	return credential, err
}

func (s *authStore) principalForUser(ctx context.Context, user authUser) (platform.Principal, error) {
	menus := append([]string(nil), adminMenus...)
	vehicles := []string{}
	vehicleGrants := []platform.VehicleGrant{}
	if user.UserType == "customer" {
		menus = []string{}
		rows, err := s.db.QueryContext(ctx, `SELECT menu_key FROM platform_user_menu WHERE user_id=? ORDER BY menu_key`, user.ID)
		if err != nil {
			return platform.Principal{}, err
		}
		for rows.Next() {
			var value string
			if err := rows.Scan(&value); err != nil {
				rows.Close()
				return platform.Principal{}, err
			}
			if customerMenuSet[value] {
				menus = append(menus, value)
			}
		}
		if err := rows.Close(); err != nil {
			return platform.Principal{}, err
		}
		rows, err = s.db.QueryContext(ctx, `SELECT vin,COALESCE(valid_from,granted_at),valid_to FROM platform_user_vehicle WHERE user_id=? AND (valid_from IS NULL OR valid_from<=NOW(3)) AND (valid_to IS NULL OR valid_to>NOW(3)) ORDER BY vin`, user.ID)
		if err != nil {
			return platform.Principal{}, err
		}
		for rows.Next() {
			var vin string
			var validFrom time.Time
			var validTo sql.NullTime
			if err := rows.Scan(&vin, &validFrom, &validTo); err != nil {
				rows.Close()
				return platform.Principal{}, err
			}
			vehicles = append(vehicles, vin)
			grant := platform.VehicleGrant{VIN: vin, ValidFrom: validFrom}
			if validTo.Valid {
				grant.ValidTo = &validTo.Time
			}
			vehicleGrants = append(vehicleGrants, grant)
		}
		if err := rows.Close(); err != nil {
			return platform.Principal{}, err
		}
	}
	return platform.Principal{
		SubjectID: strconv.FormatUint(user.ID, 10), Name: user.DisplayName, Username: user.Username,
		Role: user.UserType, UserType: user.UserType, CustomerRef: user.CustomerRef, TenantRef: user.TenantRef,
		AuthProvider: user.AuthProvider, MenuKeys: menus, VehicleVINs: vehicles, VehicleGrants: vehicleGrants,
	}, nil
}

func (s *authStore) handleAdmin(w http.ResponseWriter, r *http.Request, principal platform.Principal) bool {
	if r.URL.Path != "/api/v2/admin/users" && !strings.HasPrefix(r.URL.Path, "/api/v2/admin/users/") {
		return false
	}
	if principal.UserType != "admin" && principal.Role != "admin" {
		httpx.WriteError(w, http.StatusForbidden, "PERMISSION_DENIED", "仅管理员可以管理账号与权限", "", requestTraceID(r))
		return true
	}
	switch {
	case r.Method == http.MethodGet && r.URL.Path == "/api/v2/admin/users":
		s.listUsers(w, r)
	case r.Method == http.MethodPost && r.URL.Path == "/api/v2/admin/users":
		s.createCustomer(w, r, principal)
	case r.Method == http.MethodPut && strings.HasPrefix(r.URL.Path, "/api/v2/admin/users/"):
		s.updateCustomer(w, r, principal)
	default:
		httpx.WriteError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "不支持的账号管理操作", "", requestTraceID(r))
	}
	return true
}

func (s *authStore) listUsers(w http.ResponseWriter, r *http.Request) {
	rows, err := s.db.QueryContext(r.Context(), `SELECT id,username,display_name,user_type,status,customer_ref,tenant_ref,auth_provider,external_subject,last_login_at,created_at,updated_at FROM platform_user ORDER BY user_type ASC,created_at DESC`)
	if err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "USER_LIST_FAILED", "无法读取账号列表", "", requestTraceID(r))
		return
	}
	defer rows.Close()
	users := []authUser{}
	allVINs := []string{}
	for rows.Next() {
		var user authUser
		var external sql.NullString
		if err := rows.Scan(&user.ID, &user.Username, &user.DisplayName, &user.UserType, &user.Status, &user.CustomerRef, &user.TenantRef, &user.AuthProvider, &external, &user.LastLoginAt, &user.CreatedAt, &user.UpdatedAt); err != nil {
			httpx.WriteError(w, http.StatusInternalServerError, "USER_LIST_FAILED", "无法读取账号列表", "", requestTraceID(r))
			return
		}
		user.ExternalSubject = external.String
		principal, err := s.principalForUser(r.Context(), user)
		if err != nil {
			httpx.WriteError(w, http.StatusInternalServerError, "USER_LIST_FAILED", "无法读取账号权限", "", requestTraceID(r))
			return
		}
		user.MenuKeys = principal.MenuKeys
		if user.UserType == "customer" {
			user.Vehicles, user.GrantHistory, err = loadUserVehicleAuthorization(r.Context(), s.db, user.ID)
			if err != nil {
				httpx.WriteError(w, http.StatusInternalServerError, "USER_LIST_FAILED", "无法读取车辆授权履历", "", requestTraceID(r))
				return
			}
			user.VehicleVINs = make([]string, 0, len(user.Vehicles))
			for _, vehicle := range user.Vehicles {
				user.VehicleVINs = append(user.VehicleVINs, vehicle.VIN)
				allVINs = append(allVINs, vehicle.VIN)
			}
			for _, history := range user.GrantHistory {
				allVINs = append(allVINs, history.VIN)
			}
		}
		users = append(users, user)
	}
	if err := rows.Err(); err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "USER_LIST_FAILED", "无法读取账号列表", "", requestTraceID(r))
		return
	}
	vehicleLabels, err := loadAuthVehicleGrants(r.Context(), s.db, normalizeVINs(allVINs))
	if err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "USER_LIST_FAILED", "无法读取车辆权限信息", "", requestTraceID(r))
		return
	}
	for index := range users {
		for vehicleIndex := range users[index].Vehicles {
			users[index].Vehicles[vehicleIndex].Plate = vehicleLabels[users[index].Vehicles[vehicleIndex].VIN].Plate
		}
		for historyIndex := range users[index].GrantHistory {
			users[index].GrantHistory[historyIndex].Plate = vehicleLabels[users[index].GrantHistory[historyIndex].VIN].Plate
		}
	}
	httpx.WriteOK(w, requestTraceID(r), users)
}

func loadUserVehicleAuthorization(ctx context.Context, db *sql.DB, userID uint64) ([]authVehicleGrant, []authVehicleGrantHistory, error) {
	rows, err := db.QueryContext(ctx, `SELECT vin,COALESCE(valid_from,granted_at),valid_to,source_system,granted_by
FROM platform_user_vehicle WHERE user_id=? ORDER BY vin`, userID)
	if err != nil {
		return nil, nil, err
	}
	grants := []authVehicleGrant{}
	for rows.Next() {
		var grant authVehicleGrant
		var validTo sql.NullTime
		if err := rows.Scan(&grant.VIN, &grant.ValidFrom, &validTo, &grant.SourceSystem, &grant.GrantedBy); err != nil {
			rows.Close()
			return nil, nil, err
		}
		grant.VIN = strings.ToUpper(strings.TrimSpace(grant.VIN))
		if validTo.Valid {
			value := validTo.Time
			grant.ValidTo = &value
		}
		grants = append(grants, grant)
	}
	if err := rows.Close(); err != nil {
		return nil, nil, err
	}
	historyRows, err := db.QueryContext(ctx, `SELECT id,vin,valid_from,valid_to,source_system,granted_by,revoked_by,created_at,updated_at
FROM platform_user_vehicle_grant_history WHERE user_id=? ORDER BY valid_from DESC,id DESC`, userID)
	if err != nil {
		return nil, nil, err
	}
	history := []authVehicleGrantHistory{}
	for historyRows.Next() {
		var item authVehicleGrantHistory
		var validTo sql.NullTime
		if err := historyRows.Scan(&item.ID, &item.VIN, &item.ValidFrom, &validTo, &item.SourceSystem, &item.GrantedBy, &item.RevokedBy, &item.CreatedAt, &item.UpdatedAt); err != nil {
			historyRows.Close()
			return nil, nil, err
		}
		item.VIN = strings.ToUpper(strings.TrimSpace(item.VIN))
		if validTo.Valid {
			value := validTo.Time
			item.ValidTo = &value
		}
		history = append(history, item)
	}
	if err := historyRows.Close(); err != nil {
		return nil, nil, err
	}
	return grants, history, nil
}

func loadAuthVehicleGrants(ctx context.Context, db *sql.DB, vins []string) (map[string]authVehicleGrant, error) {
	result := make(map[string]authVehicleGrant, len(vins))
	for start := 0; start < len(vins); start += 250 {
		end := start + 250
		if end > len(vins) {
			end = len(vins)
		}
		part := vins[start:end]
		placeholders := strings.TrimSuffix(strings.Repeat("?,", len(part)), ",")
		args := make([]any, 0, len(part)*2)
		for range 2 {
			for _, vin := range part {
				args = append(args, vin)
			}
		}
		query := `SELECT vin, COALESCE(` +
			`MAX(CASE WHEN source_priority=0 THEN NULLIF(plate,'') END),` +
			`MAX(CASE WHEN source_priority=1 THEN NULLIF(plate,'') END),'') ` +
			`FROM (` +
			`SELECT UPPER(TRIM(vin)) AS vin,plate,0 AS source_priority FROM vehicle_identity_binding WHERE vin IN (` + placeholders + `) ` +
			`UNION ALL ` +
			`SELECT UPPER(TRIM(vin)) AS vin,plate,1 AS source_priority FROM vehicle_realtime_snapshot WHERE vin IN (` + placeholders + `)` +
			`) vehicle_grants GROUP BY vin`
		rows, err := db.QueryContext(ctx, query, args...)
		if err != nil {
			return nil, err
		}
		for rows.Next() {
			var vehicle authVehicleGrant
			if err := rows.Scan(&vehicle.VIN, &vehicle.Plate); err != nil {
				rows.Close()
				return nil, err
			}
			vehicle.VIN = strings.ToUpper(strings.TrimSpace(vehicle.VIN))
			vehicle.Plate = strings.TrimSpace(vehicle.Plate)
			result[vehicle.VIN] = vehicle
		}
		if err := rows.Close(); err != nil {
			return nil, err
		}
	}
	return result, nil
}

func (s *authStore) createCustomer(w http.ResponseWriter, r *http.Request, actor platform.Principal) {
	var input userMutation
	if !decodeAuthJSON(w, r, &input) {
		return
	}
	input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "enabled")
	menus, grants, err := s.validateMutation(r.Context(), input, true)
	if err != nil {
		httpx.WriteError(w, http.StatusBadRequest, "USER_INPUT_INVALID", err.Error(), "", requestTraceID(r))
		return
	}
	hash, _ := bcrypt.GenerateFromPassword([]byte(input.Password), 12)
	tx, err := s.db.BeginTx(r.Context(), nil)
	if err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "USER_CREATE_FAILED", "无法创建客户账号", "", requestTraceID(r))
		return
	}
	defer tx.Rollback()
	result, err := tx.ExecContext(r.Context(), `INSERT INTO platform_user(username,display_name,password_hash,user_type,status,customer_ref,tenant_ref,auth_provider,created_by,updated_by) VALUES(?,?,?,'customer',?,?,?,'local',?,?)`, strings.TrimSpace(input.Username), strings.TrimSpace(input.DisplayName), string(hash), input.Status, strings.TrimSpace(input.CustomerRef), strings.TrimSpace(input.TenantRef), actor.Name, actor.Name)
	if err != nil {
		writeUserMutationError(w, r, err, "创建")
		return
	}
	id, _ := result.LastInsertId()
	grantChanges := []vehicleGrantAuditChange{}
	if err := replaceGrants(r.Context(), tx, uint64(id), menus, grants, actor.Name, &grantChanges); err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "USER_CREATE_FAILED", "无法保存客户权限", "", requestTraceID(r))
		return
	}
	if err := tx.Commit(); err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "USER_CREATE_FAILED", "无法创建客户账号", "", requestTraceID(r))
		return
	}
	s.audit(r.Context(), actor.Name, "user.create", "user", strconv.FormatInt(id, 10), "success", map[string]any{"menus": menus, "vehicleGrantChanges": grantChanges}, remoteAddress(r))
	httpx.WriteOK(w, requestTraceID(r), map[string]any{"id": id})
}

func (s *authStore) updateCustomer(w http.ResponseWriter, r *http.Request, actor platform.Principal) {
	idText := strings.TrimPrefix(r.URL.Path, "/api/v2/admin/users/")
	id, err := strconv.ParseUint(idText, 10, 64)
	if err != nil || id == 0 {
		httpx.WriteError(w, http.StatusBadRequest, "USER_ID_INVALID", "账号编号无效", "", requestTraceID(r))
		return
	}
	var input userMutation
	if !decodeAuthJSON(w, r, &input) {
		return
	}
	input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "enabled")
	menus, grants, err := s.validateMutation(r.Context(), input, false)
	if err != nil {
		httpx.WriteError(w, http.StatusBadRequest, "USER_INPUT_INVALID", err.Error(), "", requestTraceID(r))
		return
	}
	tx, err := s.db.BeginTx(r.Context(), nil)
	if err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "USER_UPDATE_FAILED", "无法更新客户账号", "", requestTraceID(r))
		return
	}
	defer tx.Rollback()
	var userType string
	if err := tx.QueryRowContext(r.Context(), `SELECT user_type FROM platform_user WHERE id=? FOR UPDATE`, id).Scan(&userType); err != nil || userType != "customer" {
		httpx.WriteError(w, http.StatusBadRequest, "CUSTOMER_USER_REQUIRED", "只能通过此功能维护客户账号", "", requestTraceID(r))
		return
	}
	args := []any{strings.TrimSpace(input.DisplayName), input.Status, strings.TrimSpace(input.CustomerRef), strings.TrimSpace(input.TenantRef), actor.Name}
	query := `UPDATE platform_user SET display_name=?,status=?,customer_ref=?,tenant_ref=?,updated_by=?`
	if input.Password != "" {
		hash, _ := bcrypt.GenerateFromPassword([]byte(input.Password), 12)
		query += `,password_hash=?,password_changed_at=NOW(3),failed_login_count=0,locked_until=NULL`
		args = append(args, string(hash))
	}
	query += ` WHERE id=?`
	args = append(args, id)
	if _, err := tx.ExecContext(r.Context(), query, args...); err != nil {
		writeUserMutationError(w, r, err, "更新")
		return
	}
	grantChanges := []vehicleGrantAuditChange{}
	if err := replaceGrants(r.Context(), tx, id, menus, grants, actor.Name, &grantChanges); err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "USER_UPDATE_FAILED", "无法保存客户权限", "", requestTraceID(r))
		return
	}
	if input.Status == "disabled" || input.Password != "" {
		if _, err := tx.ExecContext(r.Context(), `UPDATE platform_user_session SET revoked_at=NOW(3) WHERE user_id=? AND revoked_at IS NULL`, id); err != nil {
			httpx.WriteError(w, http.StatusInternalServerError, "USER_UPDATE_FAILED", "无法撤销旧会话", "", requestTraceID(r))
			return
		}
	}
	if err := tx.Commit(); err != nil {
		httpx.WriteError(w, http.StatusInternalServerError, "USER_UPDATE_FAILED", "无法更新客户账号", "", requestTraceID(r))
		return
	}
	s.invalidateUser(id)
	s.audit(r.Context(), actor.Name, "user.update", "user", idText, "success", map[string]any{"status": input.Status, "menus": menus, "vehicleGrantChanges": grantChanges, "passwordReset": input.Password != ""}, remoteAddress(r))
	httpx.WriteOK(w, requestTraceID(r), map[string]any{"id": id})
}

func (s *authStore) validateMutation(ctx context.Context, input userMutation, requirePassword bool) ([]string, []vehicleGrantMutation, error) {
	if requirePassword && !usernamePattern.MatchString(strings.TrimSpace(input.Username)) {
		return nil, nil, fmt.Errorf("用户名需为 3-64 位字母、数字、点、下划线或短横线")
	}
	if strings.TrimSpace(input.DisplayName) == "" || len([]rune(strings.TrimSpace(input.DisplayName))) > 48 {
		return nil, nil, fmt.Errorf("客户名称不能为空且不能超过 48 个字符")
	}
	if input.Status != "enabled" && input.Status != "disabled" {
		return nil, nil, fmt.Errorf("账号状态无效")
	}
	if requirePassword || input.Password != "" {
		if err := validatePassword(input.Password); err != nil {
			return nil, nil, err
		}
	}
	menus := normalizeMenus(input.MenuKeys)
	if len(menus) == 0 {
		return nil, nil, fmt.Errorf("至少分配一个客户菜单")
	}
	grants, err := normalizeVehicleGrantMutations(input)
	if err != nil {
		return nil, nil, err
	}
	if len(grants) == 0 {
		return nil, nil, fmt.Errorf("至少分配一辆可查看车辆")
	}
	if len(grants) > 5000 {
		return nil, nil, fmt.Errorf("单个客户最多分配 5000 辆车")
	}
	vins := grantVINs(grants)
	found, err := existingVINs(ctx, s.db, vins)
	if err != nil {
		return nil, nil, fmt.Errorf("校验车辆失败")
	}
	if len(found) != len(vins) {
		missing := []string{}
		for _, vin := range vins {
			if !found[vin] {
				missing = append(missing, vin)
			}
			if len(missing) == 5 {
				break
			}
		}
		return nil, nil, fmt.Errorf("存在未接入的 VIN：%s", strings.Join(missing, "、"))
	}
	return menus, grants, nil
}

type currentVehicleGrant struct {
	ValidFrom        time.Time
	ValidTo          *time.Time
	HistoryID        uint64
	HistoryValidFrom *time.Time
	HistoryValidTo   *time.Time
}

func replaceGrants(ctx context.Context, tx *sql.Tx, userID uint64, menus []string, grants []vehicleGrantMutation, actor string, changes *[]vehicleGrantAuditChange) error {
	if _, err := tx.ExecContext(ctx, `DELETE FROM platform_user_menu WHERE user_id=?`, userID); err != nil {
		return err
	}
	for _, menu := range menus {
		if _, err := tx.ExecContext(ctx, `INSERT INTO platform_user_menu(user_id,menu_key,granted_by) VALUES(?,?,?)`, userID, menu, actor); err != nil {
			return err
		}
	}
	current := map[string]currentVehicleGrant{}
	rows, err := tx.QueryContext(ctx, `SELECT p.vin,COALESCE(p.valid_from,p.granted_at),p.valid_to,
COALESCE(h.id,0),h.valid_from,h.valid_to
FROM platform_user_vehicle p
LEFT JOIN platform_user_vehicle_grant_history h
  ON h.id=(SELECT MAX(latest.id) FROM platform_user_vehicle_grant_history latest WHERE latest.user_id=p.user_id AND latest.vin=p.vin)
WHERE p.user_id=? FOR UPDATE`, userID)
	if err != nil {
		return err
	}
	for rows.Next() {
		var vin string
		var validFrom time.Time
		var validTo sql.NullTime
		var historyID uint64
		var historyValidFrom, historyValidTo sql.NullTime
		if err := rows.Scan(&vin, &validFrom, &validTo, &historyID, &historyValidFrom, &historyValidTo); err != nil {
			rows.Close()
			return err
		}
		item := currentVehicleGrant{ValidFrom: validFrom, HistoryID: historyID}
		if validTo.Valid {
			value := validTo.Time
			item.ValidTo = &value
		}
		if historyValidFrom.Valid {
			value := historyValidFrom.Time
			item.HistoryValidFrom = &value
		}
		if historyValidTo.Valid {
			value := historyValidTo.Time
			item.HistoryValidTo = &value
		}
		current[strings.ToUpper(strings.TrimSpace(vin))] = item
	}
	if err := rows.Close(); err != nil {
		return err
	}
	selected := make(map[string]vehicleGrantMutation, len(grants))
	for _, grant := range grants {
		selected[grant.VIN] = grant
	}
	removedHistoryIDs := []uint64{}
	for vin, item := range current {
		if _, kept := selected[vin]; kept {
			continue
		}
		if item.HistoryID == 0 {
			return fmt.Errorf("active vehicle grant %s is missing history", vin)
		}
		if item.HistoryID > 0 {
			removedHistoryIDs = append(removedHistoryIDs, item.HistoryID)
			appendVehicleGrantChange(changes, vin, &item, nil)
		}
	}
	for _, historyID := range removedHistoryIDs {
		if _, err := tx.ExecContext(ctx, `UPDATE platform_user_vehicle_grant_history
SET valid_to=CASE WHEN valid_to IS NOT NULL AND valid_to<=NOW(3) THEN valid_to WHEN valid_from>NOW(3) THEN valid_from ELSE NOW(3) END,
revoked_by=?,updated_at=NOW(3) WHERE id=?`, actor, historyID); err != nil {
			return err
		}
	}
	if len(grants) == 0 {
		if _, err := tx.ExecContext(ctx, `DELETE FROM platform_user_vehicle WHERE user_id=?`, userID); err != nil {
			return err
		}
	} else {
		vins := grantVINs(grants)
		placeholders := strings.TrimSuffix(strings.Repeat("?,", len(vins)), ",")
		args := make([]any, 0, len(vins)+1)
		args = append(args, userID)
		for _, vin := range vins {
			args = append(args, vin)
		}
		if _, err := tx.ExecContext(ctx, `DELETE FROM platform_user_vehicle WHERE user_id=? AND vin NOT IN (`+placeholders+`)`, args...); err != nil {
			return err
		}
	}
	now := time.Now()
	for _, grant := range grants {
		existing, exists := current[grant.VIN]
		validFrom := now
		validTo := grant.ValidTo
		if grant.ValidFrom != nil {
			validFrom = *grant.ValidFrom
		} else if exists {
			validFrom = existing.ValidFrom
			validTo = existing.ValidTo
		}
		if exists {
			projectionMatches := existing.ValidFrom.Equal(validFrom) && equalOptionalTimes(existing.ValidTo, validTo)
			historyMatches := existing.HistoryValidFrom != nil && existing.HistoryValidFrom.Equal(validFrom) && equalOptionalTimes(existing.HistoryValidTo, validTo)
			if projectionMatches && historyMatches {
				continue
			}
			if _, err := tx.ExecContext(ctx, `UPDATE platform_user_vehicle SET valid_from=?,valid_to=?,source_system='manual',granted_by=? WHERE user_id=? AND vin=?`,
				validFrom, nullableTime(validTo), actor, userID, grant.VIN); err != nil {
				return err
			}
			if existing.HistoryID == 0 {
				return fmt.Errorf("active vehicle grant %s is missing history", grant.VIN)
			}
			if _, err := tx.ExecContext(ctx, `UPDATE platform_user_vehicle_grant_history
SET valid_from=?,valid_to=?,source_system='manual',granted_by=?,revoked_by='',updated_at=NOW(3) WHERE id=?`,
				validFrom, nullableTime(validTo), actor, existing.HistoryID); err != nil {
				return err
			}
			after := currentVehicleGrant{ValidFrom: validFrom, ValidTo: validTo, HistoryID: existing.HistoryID}
			appendVehicleGrantChange(changes, grant.VIN, &existing, &after)
			continue
		}
		if _, err := tx.ExecContext(ctx, `INSERT INTO platform_user_vehicle(user_id,vin,valid_from,valid_to,source_system,granted_by) VALUES(?,?,?,?,'manual',?)`,
			userID, grant.VIN, validFrom, nullableTime(validTo), actor); err != nil {
			return err
		}
		if _, err := tx.ExecContext(ctx, `INSERT INTO platform_user_vehicle_grant_history(user_id,vin,valid_from,valid_to,source_system,external_ref,granted_by)
VALUES(?,?,?,?, 'manual','',?)`, userID, grant.VIN, validFrom, nullableTime(validTo), actor); err != nil {
			return err
		}
		after := currentVehicleGrant{ValidFrom: validFrom, ValidTo: validTo}
		appendVehicleGrantChange(changes, grant.VIN, nil, &after)
	}
	return nil
}

func existingVINs(ctx context.Context, db *sql.DB, vins []string) (map[string]bool, error) {
	result := map[string]bool{}
	for start := 0; start < len(vins); start += 500 {
		end := start + 500
		if end > len(vins) {
			end = len(vins)
		}
		part := vins[start:end]
		placeholders := strings.TrimSuffix(strings.Repeat("?,", len(part)), ",")
		args := make([]any, len(part))
		for index := range part {
			args[index] = part[index]
		}
		rows, err := db.QueryContext(ctx, `SELECT vin FROM (SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin<>'' UNION SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin<>'') v WHERE vin IN (`+placeholders+`)`, args...)
		if err != nil {
			return nil, err
		}
		for rows.Next() {
			var vin string
			if err := rows.Scan(&vin); err != nil {
				rows.Close()
				return nil, err
			}
			result[strings.ToUpper(strings.TrimSpace(vin))] = true
		}
		if err := rows.Close(); err != nil {
			return nil, err
		}
	}
	return result, nil
}

func normalizeMenus(values []string) []string {
	seen := map[string]bool{}
	menus := []string{}
	for _, value := range values {
		value = strings.ToLower(strings.TrimSpace(value))
		if customerMenuSet[value] && !seen[value] {
			seen[value] = true
			menus = append(menus, value)
		}
	}
	sort.Strings(menus)
	return menus
}

func normalizeVINs(values []string) []string {
	seen := map[string]bool{}
	vins := []string{}
	for _, value := range values {
		for _, part := range strings.FieldsFunc(value, func(r rune) bool {
			return r == ',' || r == '，' || r == ';' || r == '；' || r == '\n' || r == '\t' || r == ' '
		}) {
			vin := strings.ToUpper(strings.TrimSpace(part))
			if vin != "" && len(vin) <= 64 && !seen[vin] {
				seen[vin] = true
				vins = append(vins, vin)
			}
		}
	}
	sort.Strings(vins)
	return vins
}

func normalizeVehicleGrantMutations(input userMutation) ([]vehicleGrantMutation, error) {
	if len(input.VehicleGrants) == 0 {
		vins := normalizeVINs(input.VehicleVINs)
		grants := make([]vehicleGrantMutation, 0, len(vins))
		for _, vin := range vins {
			grants = append(grants, vehicleGrantMutation{VIN: vin})
		}
		return grants, nil
	}
	seen := map[string]bool{}
	grants := make([]vehicleGrantMutation, 0, len(input.VehicleGrants))
	for _, raw := range input.VehicleGrants {
		vin := strings.ToUpper(strings.TrimSpace(raw.VIN))
		if vin == "" || len(vin) > 64 {
			return nil, fmt.Errorf("车辆授权包含无效 VIN")
		}
		if seen[vin] {
			return nil, fmt.Errorf("车辆 %s 的授权区间重复", vin)
		}
		seen[vin] = true
		validFrom, err := parseVehicleGrantTime(raw.ValidFrom, true)
		if err != nil {
			return nil, fmt.Errorf("车辆 %s 的启用时间无效：%v", vin, err)
		}
		validTo, err := parseVehicleGrantTime(raw.ValidTo, false)
		if err != nil {
			return nil, fmt.Errorf("车辆 %s 的停用时间无效：%v", vin, err)
		}
		if validTo != nil && !validTo.After(*validFrom) {
			return nil, fmt.Errorf("车辆 %s 的停用时间必须晚于启用时间", vin)
		}
		grants = append(grants, vehicleGrantMutation{VIN: vin, ValidFrom: validFrom, ValidTo: validTo})
	}
	sort.Slice(grants, func(i, j int) bool { return grants[i].VIN < grants[j].VIN })
	return grants, nil
}

func parseVehicleGrantTime(value string, required bool) (*time.Time, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		if required {
			return nil, fmt.Errorf("不能为空")
		}
		return nil, nil
	}
	if parsed, err := time.Parse(time.RFC3339Nano, value); err == nil {
		return &parsed, nil
	}
	shanghai := time.FixedZone("Asia/Shanghai", 8*60*60)
	for _, layout := range []string{"2006-01-02T15:04:05.999999999", "2006-01-02T15:04:05", "2006-01-02T15:04", "2006-01-02 15:04:05", "2006-01-02"} {
		if parsed, err := time.ParseInLocation(layout, value, shanghai); err == nil {
			return &parsed, nil
		}
	}
	return nil, fmt.Errorf("应为有效日期时间")
}

func grantVINs(grants []vehicleGrantMutation) []string {
	vins := make([]string, 0, len(grants))
	for _, grant := range grants {
		vins = append(vins, grant.VIN)
	}
	return vins
}

func nullableTime(value *time.Time) any {
	if value == nil {
		return nil
	}
	return *value
}

func equalOptionalTimes(left, right *time.Time) bool {
	if left == nil || right == nil {
		return left == nil && right == nil
	}
	return left.Equal(*right)
}

func appendVehicleGrantChange(changes *[]vehicleGrantAuditChange, vin string, before, after *currentVehicleGrant) {
	if changes == nil {
		return
	}
	change := vehicleGrantAuditChange{VIN: vin}
	if before != nil {
		change.Before = &vehicleGrantAuditSnapshot{ValidFrom: before.ValidFrom, ValidTo: before.ValidTo}
	}
	if after != nil {
		change.After = &vehicleGrantAuditSnapshot{ValidFrom: after.ValidFrom, ValidTo: after.ValidTo}
	}
	*changes = append(*changes, change)
}

func validatePassword(password string) error {
	if len(password) < 10 || len(password) > 128 {
		return fmt.Errorf("密码长度需为 10-128 位")
	}
	classes := 0
	if regexp.MustCompile(`[a-z]`).MatchString(password) {
		classes++
	}
	if regexp.MustCompile(`[A-Z]`).MatchString(password) {
		classes++
	}
	if regexp.MustCompile(`[0-9]`).MatchString(password) {
		classes++
	}
	if regexp.MustCompile(`[^A-Za-z0-9]`).MatchString(password) {
		classes++
	}
	if classes < 3 {
		return fmt.Errorf("密码需包含大小写字母、数字、特殊字符中的至少三类")
	}
	return nil
}

func randomSessionToken() (string, [sha256.Size]byte, error) {
	var raw [32]byte
	if _, err := rand.Read(raw[:]); err != nil {
		return "", [sha256.Size]byte{}, err
	}
	token := base64.RawURLEncoding.EncodeToString(raw[:])
	return token, sha256.Sum256([]byte(token)), nil
}

func randomHex(size int) (string, error) {
	raw := make([]byte, size)
	if _, err := rand.Read(raw); err != nil {
		return "", err
	}
	return hex.EncodeToString(raw), nil
}

func (s *authStore) cacheGet(key string) (platform.Principal, bool) {
	s.cacheMu.RLock()
	entry, ok := s.cache[key]
	s.cacheMu.RUnlock()
	now := time.Now()
	if !ok || now.After(entry.expiresAt) || now.Sub(entry.cachedAt) > localSessionCacheTTL {
		return platform.Principal{}, false
	}
	return entry.principal.Clone(), true
}

func (s *authStore) cachePut(key string, principal platform.Principal, expiresAt time.Time) {
	s.cacheMu.Lock()
	s.cache[key] = cachedSession{principal: principal.Clone(), expiresAt: expiresAt, cachedAt: time.Now()}
	s.cacheMu.Unlock()
}

func (s *authStore) invalidateUser(userID uint64) {
	subject := strconv.FormatUint(userID, 10)
	s.cacheMu.Lock()
	for key, entry := range s.cache {
		if entry.principal.SubjectID == subject {
			delete(s.cache, key)
		}
	}
	s.cacheMu.Unlock()
}

func (s *authStore) audit(ctx context.Context, actor, action, targetType, targetID, result string, detail any, remote string) {
	var detailJSON any
	if detail != nil {
		if encoded, err := json.Marshal(detail); err == nil {
			detailJSON = encoded
		}
	}
	_, _ = s.db.ExecContext(ctx, `INSERT INTO platform_auth_audit(actor,action,target_type,target_id,result,detail_json,remote_addr) VALUES(?,?,?,?,?,?,?)`, truncateUTF8(actor, 96), action, targetType, targetID, result, detailJSON, truncateUTF8(remote, 96))
}

func decodeAuthJSON(w http.ResponseWriter, r *http.Request, target any) bool {
	defer r.Body.Close()
	decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<20))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		httpx.WriteError(w, http.StatusBadRequest, "BAD_JSON", "请求 JSON 解析失败", "", requestTraceID(r))
		return false
	}
	return true
}

func remoteAddress(r *http.Request) string {
	if forwarded := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-For"), ",")[0]); forwarded != "" {
		return forwarded
	}
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err == nil {
		return host
	}
	return r.RemoteAddr
}

func truncateUTF8(value string, max int) string {
	runes := []rune(strings.TrimSpace(value))
	if len(runes) > max {
		runes = runes[:max]
	}
	return string(runes)
}

func writeUserMutationError(w http.ResponseWriter, r *http.Request, err error, action string) {
	message := "无法" + action + "客户账号"
	if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
		message = "用户名已存在"
	}
	httpx.WriteError(w, http.StatusConflict, "USER_MUTATION_FAILED", message, "", requestTraceID(r))
}
