package userstore

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"golang.org/x/crypto/bcrypt"

	"aijianzhan/platform/internal/authx"
)

const (
	StatusPending = "pending"
	StatusActive  = "active"
	RolePending   = authx.Role待加入
)

type User struct {
	UserID                 int64
	TenantID               int64 // 0 = 未加入任何租户
	OrgUnitID              int64 // 0 = 未绑定组织（可见租户全量，由角色决定）
	Username               string
	Phone                  string // 绑定手机号，可作登录账号
	UsernameLoginDisabled  bool   // 已绑手机时可禁用「用户名+密码」登录
	DisplayName            string
	Role                   string
	Status                 string
	CreatedAt              time.Time
}

func (u *User) HasTenant() bool {
	return u != nil && u.TenantID > 0 && u.Status == StatusActive && u.Role != RolePending
}

func (u *User) IsPlatformAdmin() bool {
	return u != nil && authx.IsPlatformAdmin(u.Role)
}

type TenantInfo struct {
	TenantID  int64     `json:"tenant_id"`
	Name      string    `json:"name"`
	Slug      string    `json:"slug"` // 全局唯一路径前缀，如 aaa → /aaa/
	CreatedAt time.Time `json:"created_at"`
	UserCount int64     `json:"user_count"`
	AppCount  int64     `json:"app_count"`
}

type Store interface {
	Register(ctx context.Context, username, password, displayName string) (*User, error)
	EnsureBootstrapOwner(ctx context.Context, username, password, displayName, companyName string) (*User, error)
	EnsurePlatformAdmin(ctx context.Context, username, password, displayName string) (*User, error)
	Login(ctx context.Context, username, password string) (*User, error)
	GetByID(ctx context.Context, userID int64) (*User, error)
	GetByPhone(ctx context.Context, phone string) (*User, error)
	BindPhone(ctx context.Context, userID int64, phone string) (*User, error)
	SetUsernameLoginDisabled(ctx context.Context, userID int64, disabled bool) (*User, error)
	PhoneExists(ctx context.Context, phone string, excludeUserID int64) (bool, error)
	JoinTenant(ctx context.Context, userID, tenantID int64, role string, orgUnitID int64) (*User, error)
	CreateTenantAsOwner(ctx context.Context, userID int64, tenantName string) (*User, error)
	SetOrgUnit(ctx context.Context, userID, tenantID, orgUnitID int64) (*User, error)
	ListTenants(ctx context.Context) ([]TenantInfo, error)
	CreateTenant(ctx context.Context, name, slug string) (*TenantInfo, error)
	GetTenant(ctx context.Context, tenantID int64) (*TenantInfo, error)
	GetTenantBySlug(ctx context.Context, slug string) (*TenantInfo, error)
	UpdateTenant(ctx context.Context, tenantID int64, name, slug string) (*TenantInfo, error)
	RenameTenantIf(ctx context.Context, oldName, newName string) error
	EnsureTenantSlugs(ctx context.Context) error
	ListMembers(ctx context.Context, tenantID int64) ([]User, error)
	UpdateMember(ctx context.Context, tenantID, userID int64, role string, orgUnitID int64, status string) (*User, error)
	// CreateMember 在指定公司直接创建登录账号；password 为空则随机生成。返回明文密码（仅此一次）。
	CreateMember(ctx context.Context, tenantID int64, username, password, displayName, role string, orgUnitID int64) (*User, string, error)
	UsernameExists(ctx context.Context, username string) (bool, error)
	// ChangePassword 校验旧密码后设置新密码（用户自行改密）。
	ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error
	// SetPassword 管理员强制设密；password 为空则随机生成。返回明文密码。
	SetPassword(ctx context.Context, userID int64, password string) (string, error)
}

type MemoryStore struct {
	mu       sync.Mutex
	users    map[string]*memUser
	byID     map[int64]*memUser
	tenants  map[int64]*TenantInfo
	seqUser  int64
	seqTen   int64
}

type memUser struct {
	User
	Hash string
}

func NewMemoryStore() *MemoryStore {
	return &MemoryStore{
		users:   map[string]*memUser{},
		byID:    map[int64]*memUser{},
		tenants: map[int64]*TenantInfo{},
	}
}

func (s *MemoryStore) Register(_ context.Context, username, password, displayName string) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.users[username]; ok {
		return nil, fmt.Errorf("username already exists")
	}
	if len(username) < 3 || len(password) < 6 {
		return nil, fmt.Errorf("username>=3 and password>=6 required")
	}
	hash, err := hashPassword(password)
	if err != nil {
		return nil, err
	}
	name := displayName
	if name == "" {
		name = username
	}
	s.seqUser++
	u := &memUser{
		User: User{
			UserID: s.seqUser, TenantID: 0, Username: username,
			DisplayName: name, Role: RolePending, Status: StatusPending, CreatedAt: time.Now().UTC(),
		},
		Hash: hash,
	}
	s.users[username] = u
	s.byID[u.UserID] = u
	cp := u.User
	return &cp, nil
}

func (s *MemoryStore) EnsureBootstrapOwner(_ context.Context, username, password, displayName, companyName string) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if existing, ok := s.users[username]; ok {
		cp := existing.User
		return &cp, nil
	}
	hash, err := hashPassword(password)
	if err != nil {
		return nil, err
	}
	name := displayName
	if name == "" {
		name = username
	}
	tenName := strings.TrimSpace(companyName)
	if tenName == "" {
		tenName = name
	}
	s.seqTen++
	s.seqUser++
	u := &memUser{
		User: User{
			UserID: s.seqUser, TenantID: s.seqTen, Username: username,
			DisplayName: name, Role: authx.Role管理员, Status: StatusActive, CreatedAt: time.Now().UTC(),
		},
		Hash: hash,
	}
	s.users[username] = u
	s.byID[u.UserID] = u
	s.tenants[s.seqTen] = &TenantInfo{TenantID: s.seqTen, Name: tenName, Slug: "demo", CreatedAt: time.Now().UTC()}
	cp := u.User
	return &cp, nil
}

func (s *MemoryStore) EnsurePlatformAdmin(_ context.Context, username, password, displayName string) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	hash, err := hashPassword(password)
	if err != nil {
		return nil, err
	}
	name := displayName
	if name == "" {
		name = username
	}
	if existing, ok := s.users[username]; ok {
		existing.Hash = hash
		existing.Role = authx.Role超级管理员
		existing.Status = StatusActive
		existing.TenantID = 0
		existing.OrgUnitID = 0
		if name != "" {
			existing.DisplayName = name
		}
		cp := existing.User
		return &cp, nil
	}
	s.seqUser++
	u := &memUser{
		User: User{
			UserID: s.seqUser, TenantID: 0, Username: username,
			DisplayName: name, Role: authx.Role超级管理员, Status: StatusActive, CreatedAt: time.Now().UTC(),
		},
		Hash: hash,
	}
	s.users[username] = u
	s.byID[u.UserID] = u
	cp := u.User
	return &cp, nil
}

func (s *MemoryStore) ListTenants(_ context.Context) ([]TenantInfo, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	out := make([]TenantInfo, 0, len(s.tenants))
	for _, t := range s.tenants {
		info := *t
		var uc int64
		for _, u := range s.users {
			if u.TenantID == t.TenantID && u.Status == StatusActive {
				uc++
			}
		}
		info.UserCount = uc
		out = append(out, info)
	}
	return out, nil
}

func (s *MemoryStore) GetTenant(_ context.Context, tenantID int64) (*TenantInfo, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	t, ok := s.tenants[tenantID]
	if !ok {
		return nil, fmt.Errorf("tenant not found")
	}
	cp := *t
	return &cp, nil
}

func (s *MemoryStore) CreateTenant(_ context.Context, name, slug string) (*TenantInfo, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	name = strings.TrimSpace(name)
	if name == "" {
		return nil, fmt.Errorf("name required")
	}
	if strings.TrimSpace(slug) == "" {
		slug = SuggestTenantSlug(name)
	}
	ns, err := NormalizeTenantSlug(slug)
	if err != nil {
		return nil, err
	}
	for _, t := range s.tenants {
		if t.Slug == ns {
			return nil, fmt.Errorf("slug already exists: %s", ns)
		}
	}
	s.seqTen++
	t := &TenantInfo{TenantID: s.seqTen, Name: name, Slug: ns, CreatedAt: time.Now().UTC()}
	s.tenants[s.seqTen] = t
	cp := *t
	return &cp, nil
}

func (s *MemoryStore) UpdateTenant(_ context.Context, tenantID int64, name, slug string) (*TenantInfo, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	t, ok := s.tenants[tenantID]
	if !ok {
		return nil, fmt.Errorf("tenant not found")
	}
	name = strings.TrimSpace(name)
	if name == "" {
		return nil, fmt.Errorf("name required")
	}
	t.Name = name
	if strings.TrimSpace(slug) != "" {
		ns, err := NormalizeTenantSlug(slug)
		if err != nil {
			return nil, err
		}
		for id, other := range s.tenants {
			if id != tenantID && other.Slug == ns {
				return nil, fmt.Errorf("slug already exists: %s", ns)
			}
		}
		t.Slug = ns
	}
	cp := *t
	return &cp, nil
}

func (s *MemoryStore) GetTenantBySlug(_ context.Context, slug string) (*TenantInfo, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	ns, err := NormalizeTenantSlug(slug)
	if err != nil {
		return nil, err
	}
	for _, t := range s.tenants {
		if t.Slug == ns {
			cp := *t
			return &cp, nil
		}
	}
	return nil, fmt.Errorf("tenant not found")
}

func (s *MemoryStore) EnsureTenantSlugs(_ context.Context) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	used := map[string]struct{}{}
	for _, t := range s.tenants {
		if t.Slug != "" {
			used[t.Slug] = struct{}{}
		}
	}
	for _, t := range s.tenants {
		if t.Slug != "" {
			continue
		}
		base := SuggestTenantSlug(t.Name)
		if base == "" {
			base = fmt.Sprintf("t%d", t.TenantID)
		}
		cand := base
		for i := 2; ; i++ {
			if _, ok := used[cand]; !ok {
				if _, err := NormalizeTenantSlug(cand); err == nil {
					break
				}
			}
			cand = fmt.Sprintf("%s-%d", base, i)
			if i > 100 {
				cand = fmt.Sprintf("t%d", t.TenantID)
				break
			}
		}
		ns, err := NormalizeTenantSlug(cand)
		if err != nil {
			ns = fmt.Sprintf("t%d", t.TenantID)
		}
		t.Slug = ns
		used[ns] = struct{}{}
	}
	return nil
}

func (s *MemoryStore) ListMembers(_ context.Context, tenantID int64) ([]User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	out := make([]User, 0)
	for _, u := range s.users {
		if u.TenantID == tenantID && !authx.IsPlatformAdmin(u.Role) {
			cp := u.User
			cp.Role = authx.NormalizeRole(cp.Role)
			out = append(out, cp)
		}
	}
	return out, nil
}

func (s *MemoryStore) UsernameExists(_ context.Context, username string) (bool, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	_, ok := s.users[strings.TrimSpace(username)]
	return ok, nil
}

func (s *MemoryStore) CreateMember(_ context.Context, tenantID int64, username, password, displayName, role string, orgUnitID int64) (*User, string, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if tenantID <= 0 {
		return nil, "", fmt.Errorf("tenant required")
	}
	if _, ok := s.tenants[tenantID]; !ok {
		return nil, "", fmt.Errorf("tenant not found")
	}
	username = strings.TrimSpace(username)
	if len(username) < 3 {
		return nil, "", fmt.Errorf("username>=3 required")
	}
	if _, ok := s.users[username]; ok {
		return nil, "", fmt.Errorf("username already exists")
	}
	plain := strings.TrimSpace(password)
	var err error
	if plain == "" {
		plain, err = RandomPassword(12)
		if err != nil {
			return nil, "", err
		}
	}
	if len(plain) < 6 {
		return nil, "", fmt.Errorf("password>=6 required")
	}
	if !authx.ValidPlatformRole(role) {
		return nil, "", fmt.Errorf("invalid role")
	}
	role = authx.NormalizeRole(role)
	hash, err := hashPassword(plain)
	if err != nil {
		return nil, "", err
	}
	name := strings.TrimSpace(displayName)
	if name == "" {
		name = username
	}
	s.seqUser++
	u := &memUser{
		User: User{
			UserID: s.seqUser, TenantID: tenantID, OrgUnitID: orgUnitID, Username: username,
			DisplayName: name, Role: role, Status: StatusActive, CreatedAt: time.Now().UTC(),
		},
		Hash: hash,
	}
	s.users[username] = u
	s.byID[u.UserID] = u
	if t := s.tenants[tenantID]; t != nil {
		t.UserCount++
	}
	cp := u.User
	return &cp, plain, nil
}

func (s *MemoryStore) ChangePassword(_ context.Context, userID int64, oldPassword, newPassword string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	u, ok := s.byID[userID]
	if !ok {
		return fmt.Errorf("user not found")
	}
	if bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(oldPassword)) != nil {
		return fmt.Errorf("旧密码不正确")
	}
	newPassword = strings.TrimSpace(newPassword)
	if len(newPassword) < 6 {
		return fmt.Errorf("password>=6 required")
	}
	hash, err := hashPassword(newPassword)
	if err != nil {
		return err
	}
	u.Hash = hash
	return nil
}

func (s *MemoryStore) SetPassword(_ context.Context, userID int64, password string) (string, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	u, ok := s.byID[userID]
	if !ok {
		return "", fmt.Errorf("user not found")
	}
	plain := strings.TrimSpace(password)
	var err error
	if plain == "" {
		plain, err = RandomPassword(12)
		if err != nil {
			return "", err
		}
	}
	if len(plain) < 6 {
		return "", fmt.Errorf("password>=6 required")
	}
	hash, err := hashPassword(plain)
	if err != nil {
		return "", err
	}
	u.Hash = hash
	return plain, nil
}

func (s *MemoryStore) UpdateMember(_ context.Context, tenantID, userID int64, role string, orgUnitID int64, status string) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	u, ok := s.byID[userID]
	if !ok || u.TenantID != tenantID {
		return nil, fmt.Errorf("member not found")
	}
	if authx.IsPlatformAdmin(u.Role) {
		return nil, fmt.Errorf("cannot edit platform admin")
	}
	if role != "" {
		if !authx.ValidPlatformRole(role) {
			return nil, fmt.Errorf("invalid role")
		}
		u.Role = authx.NormalizeRole(role)
	}
	if status == StatusActive || status == StatusPending || status == "disabled" {
		u.Status = status
	}
	u.OrgUnitID = orgUnitID
	cp := u.User
	return &cp, nil
}

func (s *MemoryStore) Login(_ context.Context, account, password string) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	account = strings.TrimSpace(account)
	u, ok := s.users[account]
	if !ok {
		if phone, err := NormalizePhone(account); err == nil {
			for _, cand := range s.users {
				if cand.Phone == phone {
					u, ok = cand, true
					break
				}
			}
		}
	}
	if !ok || bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(password)) != nil {
		return nil, fmt.Errorf("invalid username or password")
	}
	cp := u.User
	cp.Role = authx.NormalizeRole(cp.Role)
	if cp.Status == "disabled" {
		return nil, fmt.Errorf("账号已停用")
	}
	return &cp, nil
}

func (s *MemoryStore) GetByID(_ context.Context, userID int64) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	u, ok := s.byID[userID]
	if !ok {
		return nil, fmt.Errorf("user not found")
	}
	cp := u.User
	cp.Role = authx.NormalizeRole(cp.Role)
	return &cp, nil
}

func (s *MemoryStore) GetByPhone(_ context.Context, phone string) (*User, error) {
	ns, err := NormalizePhone(phone)
	if err != nil {
		return nil, err
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	for _, u := range s.users {
		if u.Phone == ns {
			cp := u.User
			cp.Role = authx.NormalizeRole(cp.Role)
			return &cp, nil
		}
	}
	return nil, fmt.Errorf("user not found")
}

func (s *MemoryStore) PhoneExists(_ context.Context, phone string, excludeUserID int64) (bool, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	phone = strings.TrimSpace(phone)
	if phone == "" {
		return false, nil
	}
	for _, u := range s.users {
		if u.Phone == phone && u.UserID != excludeUserID {
			return true, nil
		}
	}
	return false, nil
}

func (s *MemoryStore) BindPhone(_ context.Context, userID int64, phone string) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	u, ok := s.byID[userID]
	if !ok {
		return nil, fmt.Errorf("user not found")
	}
	phone = strings.TrimSpace(phone)
	if phone == "" {
		u.Phone = ""
		u.UsernameLoginDisabled = false
		cp := u.User
		return &cp, nil
	}
	ns, err := NormalizePhone(phone)
	if err != nil {
		return nil, err
	}
	for _, o := range s.users {
		if o.Phone == ns && o.UserID != userID {
			return nil, fmt.Errorf("该手机号已被绑定")
		}
	}
	u.Phone = ns
	cp := u.User
	return &cp, nil
}

func (s *MemoryStore) SetUsernameLoginDisabled(_ context.Context, userID int64, disabled bool) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	u, ok := s.byID[userID]
	if !ok {
		return nil, fmt.Errorf("user not found")
	}
	if disabled && strings.TrimSpace(u.Phone) == "" {
		return nil, fmt.Errorf("请先绑定手机号，再禁用用户名登录")
	}
	u.UsernameLoginDisabled = disabled
	cp := u.User
	return &cp, nil
}

func (s *MemoryStore) JoinTenant(_ context.Context, userID, tenantID int64, role string, orgUnitID int64) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	u, ok := s.byID[userID]
	if !ok {
		return nil, fmt.Errorf("user not found")
	}
	if u.TenantID > 0 && u.Status == StatusActive {
		return nil, fmt.Errorf("user already joined a tenant")
	}
	if tenantID <= 0 {
		return nil, fmt.Errorf("invalid tenant")
	}
	role = normalizePlatformRole(role)
	u.TenantID = tenantID
	u.OrgUnitID = orgUnitID
	u.Role = role
	u.Status = StatusActive
	cp := u.User
	return &cp, nil
}

func (s *MemoryStore) SetOrgUnit(_ context.Context, userID, tenantID, orgUnitID int64) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	u, ok := s.byID[userID]
	if !ok || u.TenantID != tenantID {
		return nil, fmt.Errorf("user not found")
	}
	u.OrgUnitID = orgUnitID
	cp := u.User
	return &cp, nil
}

func (s *MemoryStore) CreateTenantAsOwner(_ context.Context, userID int64, tenantName string) (*User, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	u, ok := s.byID[userID]
	if !ok {
		return nil, fmt.Errorf("user not found")
	}
	if u.TenantID > 0 && u.Status == StatusActive {
		return nil, fmt.Errorf("user already joined a tenant")
	}
	name := strings.TrimSpace(tenantName)
	if name == "" {
		name = u.DisplayName
	}
	if name == "" {
		name = u.Username
	}
	s.seqTen++
	u.TenantID = s.seqTen
	u.Role = authx.Role管理员
	u.Status = StatusActive
	slug := SuggestTenantSlug(name)
	if slug == "" {
		slug = fmt.Sprintf("t%d", s.seqTen)
	}
	base := slug
	for i := 2; ; i++ {
		clash := false
		for _, t := range s.tenants {
			if t.Slug == slug {
				clash = true
				break
			}
		}
		if !clash {
			if _, err := NormalizeTenantSlug(slug); err == nil {
				break
			}
		}
		slug = fmt.Sprintf("%s-%d", base, i)
		if i > 50 {
			slug = fmt.Sprintf("t%d", s.seqTen)
			break
		}
	}
	ns, _ := NormalizeTenantSlug(slug)
	if ns == "" {
		ns = fmt.Sprintf("t%d", s.seqTen)
	}
	s.tenants[s.seqTen] = &TenantInfo{TenantID: s.seqTen, Name: name, Slug: ns, CreatedAt: time.Now().UTC()}
	cp := u.User
	return &cp, nil
}

type PostgresStore struct {
	DB *sql.DB
}

func NewPostgresStore(db *sql.DB) *PostgresStore {
	return &PostgresStore{DB: db}
}

func (s *PostgresStore) Register(ctx context.Context, username, password, displayName string) (*User, error) {
	if len(username) < 3 || len(password) < 6 {
		return nil, fmt.Errorf("username>=3 and password>=6 required")
	}
	hash, err := hashPassword(password)
	if err != nil {
		return nil, err
	}
	name := displayName
	if name == "" {
		name = username
	}
	u := &User{TenantID: 0, Username: username, DisplayName: name, Role: RolePending, Status: StatusPending}
	err = s.DB.QueryRowContext(ctx, `
INSERT INTO platform_meta.users(tenant_id, username, password_hash, display_name, role, status)
VALUES(NULL,$1,$2,$3,'pending','pending')
RETURNING user_id, created_at`, username, hash, name,
	).Scan(&u.UserID, &u.CreatedAt)
	if err != nil {
		if isUnique(err) {
			return nil, fmt.Errorf("username already exists")
		}
		return nil, err
	}
	return u, nil
}

func (s *PostgresStore) EnsureBootstrapOwner(ctx context.Context, username, password, displayName, companyName string) (*User, error) {
	if u, err := s.Login(ctx, username, password); err == nil {
		return u, nil
	}
	hash, err := hashPassword(password)
	if err != nil {
		return nil, err
	}
	tx, err := s.DB.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer func() { _ = tx.Rollback() }()

	name := displayName
	if name == "" {
		name = username
	}
	tenName := strings.TrimSpace(companyName)
	if tenName == "" {
		tenName = name
	}
	tenSlug := "demo"
	if tenName != "演示公司" && tenName != "演示账号" {
		tenSlug = SuggestTenantSlug(tenName)
		if tenSlug == "" {
			tenSlug = "demo"
		}
	}
	var tenantID int64
	if err := tx.QueryRowContext(ctx,
		`INSERT INTO platform_meta.tenants(name, slug) VALUES($1,$2) RETURNING tenant_id`, tenName, tenSlug,
	).Scan(&tenantID); err != nil {
		return nil, err
	}
	u := &User{TenantID: tenantID, Username: username, DisplayName: name, Role: authx.Role管理员, Status: StatusActive}
	err = tx.QueryRowContext(ctx, `
INSERT INTO platform_meta.users(tenant_id, username, password_hash, display_name, role, status)
VALUES($1,$2,$3,$4,$5,'active')
RETURNING user_id, created_at`, tenantID, username, hash, name, authx.Role管理员,
	).Scan(&u.UserID, &u.CreatedAt)
	if err != nil {
		if isUnique(err) {
			_ = tx.Rollback()
			return s.Login(ctx, username, password)
		}
		return nil, err
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return u, nil
}

func (s *PostgresStore) EnsurePlatformAdmin(ctx context.Context, username, password, displayName string) (*User, error) {
	hash, err := hashPassword(password)
	if err != nil {
		return nil, err
	}
	name := displayName
	if name == "" {
		name = username
	}

	var userID int64
	err = s.DB.QueryRowContext(ctx, `SELECT user_id FROM platform_meta.users WHERE username=$1`, username).Scan(&userID)
	if err == nil {
		_, err = s.DB.ExecContext(ctx, `
UPDATE platform_meta.users
SET password_hash=$1, role=$2, status='active', tenant_id=NULL, org_unit_id=NULL,
    display_name=CASE WHEN $3='' THEN display_name ELSE $3 END
WHERE user_id=$4`, hash, authx.Role超级管理员, name, userID)
		if err != nil {
			return nil, err
		}
		return s.GetByID(ctx, userID)
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return nil, err
	}

	u := &User{TenantID: 0, Username: username, DisplayName: name, Role: authx.Role超级管理员, Status: StatusActive}
	err = s.DB.QueryRowContext(ctx, `
INSERT INTO platform_meta.users(tenant_id, username, password_hash, display_name, role, status)
VALUES(NULL,$1,$2,$3,$4,'active')
RETURNING user_id, created_at`, username, hash, name, authx.Role超级管理员,
	).Scan(&u.UserID, &u.CreatedAt)
	if err != nil {
		if isUnique(err) {
			// 并发创建：再走一次 upsert
			return s.EnsurePlatformAdmin(ctx, username, password, displayName)
		}
		return nil, err
	}
	return u, nil
}

func (s *PostgresStore) ListTenants(ctx context.Context) ([]TenantInfo, error) {
	rows, err := s.DB.QueryContext(ctx, `
SELECT t.tenant_id, t.name, COALESCE(t.slug,''), t.created_at,
  (SELECT COUNT(*) FROM platform_meta.users u WHERE u.tenant_id=t.tenant_id AND COALESCE(u.status,'active')='active') AS user_count,
  (SELECT COUNT(*) FROM platform_meta.tenant_apps a WHERE a.tenant_id=t.tenant_id) AS app_count
FROM platform_meta.tenants t
ORDER BY t.tenant_id`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []TenantInfo
	for rows.Next() {
		var t TenantInfo
		if err := rows.Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt, &t.UserCount, &t.AppCount); err != nil {
			return nil, err
		}
		out = append(out, t)
	}
	return out, rows.Err()
}

func (s *PostgresStore) GetTenant(ctx context.Context, tenantID int64) (*TenantInfo, error) {
	var t TenantInfo
	err := s.DB.QueryRowContext(ctx, `
SELECT t.tenant_id, t.name, COALESCE(t.slug,''), t.created_at,
  (SELECT COUNT(*) FROM platform_meta.users u WHERE u.tenant_id=t.tenant_id AND COALESCE(u.status,'active')='active'),
  (SELECT COUNT(*) FROM platform_meta.tenant_apps a WHERE a.tenant_id=t.tenant_id)
FROM platform_meta.tenants t WHERE t.tenant_id=$1`, tenantID,
	).Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt, &t.UserCount, &t.AppCount)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, fmt.Errorf("tenant not found")
	}
	if err != nil {
		return nil, err
	}
	return &t, nil
}

func (s *PostgresStore) GetTenantBySlug(ctx context.Context, slug string) (*TenantInfo, error) {
	ns, err := NormalizeTenantSlug(slug)
	if err != nil {
		return nil, err
	}
	var t TenantInfo
	err = s.DB.QueryRowContext(ctx, `
SELECT t.tenant_id, t.name, COALESCE(t.slug,''), t.created_at,
  (SELECT COUNT(*) FROM platform_meta.users u WHERE u.tenant_id=t.tenant_id AND COALESCE(u.status,'active')='active'),
  (SELECT COUNT(*) FROM platform_meta.tenant_apps a WHERE a.tenant_id=t.tenant_id)
FROM platform_meta.tenants t WHERE t.slug=$1`, ns,
	).Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt, &t.UserCount, &t.AppCount)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, fmt.Errorf("tenant not found")
	}
	if err != nil {
		return nil, err
	}
	return &t, nil
}

func (s *PostgresStore) CreateTenant(ctx context.Context, name, slug string) (*TenantInfo, error) {
	name = strings.TrimSpace(name)
	if name == "" {
		return nil, fmt.Errorf("name required")
	}
	if strings.TrimSpace(slug) == "" {
		slug = SuggestTenantSlug(name)
	}
	ns, err := NormalizeTenantSlug(slug)
	if err != nil {
		return nil, err
	}
	var t TenantInfo
	err = s.DB.QueryRowContext(ctx,
		`INSERT INTO platform_meta.tenants(name, slug) VALUES($1,$2) RETURNING tenant_id, name, slug, created_at`, name, ns,
	).Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt)
	if err != nil {
		if isUnique(err) {
			return nil, fmt.Errorf("slug already exists: %s", ns)
		}
		return nil, err
	}
	return &t, nil
}

func (s *PostgresStore) UpdateTenant(ctx context.Context, tenantID int64, name, slug string) (*TenantInfo, error) {
	name = strings.TrimSpace(name)
	if name == "" {
		return nil, fmt.Errorf("name required")
	}
	if strings.TrimSpace(slug) == "" {
		res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET name=$1 WHERE tenant_id=$2`, name, tenantID)
		if err != nil {
			return nil, err
		}
		n, _ := res.RowsAffected()
		if n == 0 {
			return nil, fmt.Errorf("tenant not found")
		}
		return s.GetTenant(ctx, tenantID)
	}
	ns, err := NormalizeTenantSlug(slug)
	if err != nil {
		return nil, err
	}
	res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET name=$1, slug=$2 WHERE tenant_id=$3`, name, ns, tenantID)
	if err != nil {
		if isUnique(err) {
			return nil, fmt.Errorf("slug already exists: %s", ns)
		}
		return nil, err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return nil, fmt.Errorf("tenant not found")
	}
	return s.GetTenant(ctx, tenantID)
}

func (s *PostgresStore) EnsureTenantSlugs(ctx context.Context) error {
	rows, err := s.DB.QueryContext(ctx, `SELECT tenant_id, name FROM platform_meta.tenants WHERE COALESCE(slug,'')='' ORDER BY tenant_id`)
	if err != nil {
		return err
	}
	defer rows.Close()
	type row struct {
		id   int64
		name string
	}
	var need []row
	for rows.Next() {
		var r row
		if err := rows.Scan(&r.id, &r.name); err != nil {
			return err
		}
		need = append(need, r)
	}
	if err := rows.Err(); err != nil {
		return err
	}
	for _, r := range need {
		base := SuggestTenantSlug(r.name)
		if base == "" {
			base = fmt.Sprintf("t%d", r.id)
		}
		cand := base
		for i := 2; i < 100; i++ {
			ns, nerr := NormalizeTenantSlug(cand)
			if nerr != nil {
				cand = fmt.Sprintf("t%d", r.id)
				ns = cand
			}
			_, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET slug=$1 WHERE tenant_id=$2 AND COALESCE(slug,'')=''`, ns, r.id)
			if err == nil {
				break
			}
			if isUnique(err) {
				cand = fmt.Sprintf("%s-%d", base, i)
				continue
			}
			return err
		}
	}
	// 演示公司固定 slug=demo（若仍空或为自动生成）
	_, _ = s.DB.ExecContext(ctx, `
UPDATE platform_meta.tenants SET slug='demo'
WHERE name IN ('演示公司','演示账号') AND (COALESCE(slug,'')='' OR slug LIKE 't%' OR slug='yan-shi')
  AND NOT EXISTS (SELECT 1 FROM platform_meta.tenants x WHERE x.slug='demo' AND x.name NOT IN ('演示公司','演示账号'))`)
	return nil
}

func (s *PostgresStore) RenameTenantIf(ctx context.Context, oldName, newName string) error {
	oldName = strings.TrimSpace(oldName)
	newName = strings.TrimSpace(newName)
	if oldName == "" || newName == "" || oldName == newName {
		return nil
	}
	_, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET name=$1 WHERE name=$2`, newName, oldName)
	return err
}

func (s *MemoryStore) RenameTenantIf(_ context.Context, oldName, newName string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	oldName = strings.TrimSpace(oldName)
	newName = strings.TrimSpace(newName)
	if oldName == "" || newName == "" {
		return nil
	}
	for _, t := range s.tenants {
		if t.Name == oldName {
			t.Name = newName
		}
	}
	return nil
}

func (s *PostgresStore) ListMembers(ctx context.Context, tenantID int64) ([]User, error) {
	rows, err := s.DB.QueryContext(ctx, `
SELECT user_id, COALESCE(tenant_id,0), COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), display_name, role, COALESCE(status,'active'), created_at
FROM platform_meta.users
WHERE tenant_id=$1
ORDER BY user_id`, tenantID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []User
	for rows.Next() {
		var u User
		if err := rows.Scan(&u.UserID, &u.TenantID, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt); err != nil {
			return nil, err
		}
		u.Role = authx.NormalizeRole(u.Role)
		if authx.IsPlatformAdmin(u.Role) {
			continue
		}
		out = append(out, u)
	}
	return out, rows.Err()
}

func (s *PostgresStore) UsernameExists(ctx context.Context, username string) (bool, error) {
	var n int
	err := s.DB.QueryRowContext(ctx, `SELECT 1 FROM platform_meta.users WHERE username=$1 LIMIT 1`, strings.TrimSpace(username)).Scan(&n)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

func (s *PostgresStore) CreateMember(ctx context.Context, tenantID int64, username, password, displayName, role string, orgUnitID int64) (*User, string, error) {
	if tenantID <= 0 {
		return nil, "", fmt.Errorf("tenant required")
	}
	if _, err := s.GetTenant(ctx, tenantID); err != nil {
		return nil, "", err
	}
	username = strings.TrimSpace(username)
	if len(username) < 3 {
		return nil, "", fmt.Errorf("username>=3 required")
	}
	plain := strings.TrimSpace(password)
	var err error
	if plain == "" {
		plain, err = RandomPassword(12)
		if err != nil {
			return nil, "", err
		}
	}
	if len(plain) < 6 {
		return nil, "", fmt.Errorf("password>=6 required")
	}
	if !authx.ValidPlatformRole(role) {
		return nil, "", fmt.Errorf("invalid role")
	}
	role = authx.NormalizeRole(role)
	hash, err := hashPassword(plain)
	if err != nil {
		return nil, "", err
	}
	name := strings.TrimSpace(displayName)
	if name == "" {
		name = username
	}
	u := &User{TenantID: tenantID, OrgUnitID: orgUnitID, Username: username, DisplayName: name, Role: role, Status: StatusActive}
	err = s.DB.QueryRowContext(ctx, `
INSERT INTO platform_meta.users(tenant_id, org_unit_id, username, password_hash, display_name, role, status)
VALUES($1,NULLIF($2,0),$3,$4,$5,$6,'active')
RETURNING user_id, created_at`, tenantID, orgUnitID, username, hash, name, role,
	).Scan(&u.UserID, &u.CreatedAt)
	if err != nil {
		if isUnique(err) {
			return nil, "", fmt.Errorf("username already exists")
		}
		return nil, "", err
	}
	return u, plain, nil
}

func (s *PostgresStore) ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error {
	var hash string
	err := s.DB.QueryRowContext(ctx, `SELECT password_hash FROM platform_meta.users WHERE user_id=$1`, userID).Scan(&hash)
	if errors.Is(err, sql.ErrNoRows) {
		return fmt.Errorf("user not found")
	}
	if err != nil {
		return err
	}
	if bcrypt.CompareHashAndPassword([]byte(hash), []byte(oldPassword)) != nil {
		return fmt.Errorf("旧密码不正确")
	}
	newPassword = strings.TrimSpace(newPassword)
	if len(newPassword) < 6 {
		return fmt.Errorf("password>=6 required")
	}
	nh, err := hashPassword(newPassword)
	if err != nil {
		return err
	}
	_, err = s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET password_hash=$1 WHERE user_id=$2`, nh, userID)
	return err
}

func (s *PostgresStore) SetPassword(ctx context.Context, userID int64, password string) (string, error) {
	plain := strings.TrimSpace(password)
	var err error
	if plain == "" {
		plain, err = RandomPassword(12)
		if err != nil {
			return "", err
		}
	}
	if len(plain) < 6 {
		return "", fmt.Errorf("password>=6 required")
	}
	nh, err := hashPassword(plain)
	if err != nil {
		return "", err
	}
	res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET password_hash=$1 WHERE user_id=$2`, nh, userID)
	if err != nil {
		return "", err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return "", fmt.Errorf("user not found")
	}
	return plain, nil
}

func (s *PostgresStore) UpdateMember(ctx context.Context, tenantID, userID int64, role string, orgUnitID int64, status string) (*User, error) {
	cur, err := s.GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if cur.TenantID != tenantID {
		return nil, fmt.Errorf("member not found")
	}
	if authx.IsPlatformAdmin(cur.Role) {
		return nil, fmt.Errorf("cannot edit platform admin")
	}
	if role != "" {
		if !authx.ValidPlatformRole(role) {
			return nil, fmt.Errorf("invalid role")
		}
		role = authx.NormalizeRole(role)
	} else {
		role = cur.Role
	}
	if status == "" {
		status = cur.Status
	}
	if status != StatusActive && status != StatusPending && status != "disabled" {
		return nil, fmt.Errorf("invalid status")
	}
	_, err = s.DB.ExecContext(ctx, `
UPDATE platform_meta.users SET role=$1, org_unit_id=NULLIF($2,0), status=$3
WHERE user_id=$4 AND tenant_id=$5`, role, orgUnitID, status, userID, tenantID)
	if err != nil {
		return nil, err
	}
	return s.GetByID(ctx, userID)
}

func (s *PostgresStore) Login(ctx context.Context, account, password string) (*User, error) {
	account = strings.TrimSpace(account)
	u, hash, err := s.loadAuthByUsername(ctx, account)
	if errors.Is(err, sql.ErrNoRows) {
		if phone, nerr := NormalizePhone(account); nerr == nil {
			u, hash, err = s.loadAuthByPhone(ctx, phone)
		}
	}
	if errors.Is(err, sql.ErrNoRows) {
		return nil, fmt.Errorf("invalid username or password")
	}
	if err != nil {
		return nil, err
	}
	if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) != nil {
		return nil, fmt.Errorf("invalid username or password")
	}
	u.Role = authx.NormalizeRole(u.Role)
	if u.Status == "disabled" {
		return nil, fmt.Errorf("账号已停用")
	}
	return u, nil
}

func (s *PostgresStore) loadAuthByUsername(ctx context.Context, username string) (*User, string, error) {
	var u User
	var hash string
	var tenant sql.NullInt64
	err := s.DB.QueryRowContext(ctx, `
SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), password_hash, display_name, role, COALESCE(status,'active'), created_at
FROM platform_meta.users WHERE username=$1`, username,
	).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &hash, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt)
	if err != nil {
		return nil, "", err
	}
	if tenant.Valid {
		u.TenantID = tenant.Int64
	}
	return &u, hash, nil
}

func (s *PostgresStore) loadAuthByPhone(ctx context.Context, phone string) (*User, string, error) {
	var u User
	var hash string
	var tenant sql.NullInt64
	err := s.DB.QueryRowContext(ctx, `
SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), password_hash, display_name, role, COALESCE(status,'active'), created_at
FROM platform_meta.users WHERE phone=$1`, phone,
	).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &hash, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt)
	if err != nil {
		return nil, "", err
	}
	if tenant.Valid {
		u.TenantID = tenant.Int64
	}
	return &u, hash, nil
}

func (s *PostgresStore) GetByID(ctx context.Context, userID int64) (*User, error) {
	var u User
	var tenant sql.NullInt64
	err := s.DB.QueryRowContext(ctx, `
SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), display_name, role, COALESCE(status,'active'), created_at
FROM platform_meta.users WHERE user_id=$1`, userID,
	).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, fmt.Errorf("user not found")
	}
	if err != nil {
		return nil, err
	}
	if tenant.Valid {
		u.TenantID = tenant.Int64
	}
	u.Role = authx.NormalizeRole(u.Role)
	return &u, nil
}

func (s *PostgresStore) GetByPhone(ctx context.Context, phone string) (*User, error) {
	ns, err := NormalizePhone(phone)
	if err != nil {
		return nil, err
	}
	var u User
	var tenant sql.NullInt64
	err = s.DB.QueryRowContext(ctx, `
SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), display_name, role, COALESCE(status,'active'), created_at
FROM platform_meta.users WHERE phone=$1`, ns,
	).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, fmt.Errorf("user not found")
	}
	if err != nil {
		return nil, err
	}
	if tenant.Valid {
		u.TenantID = tenant.Int64
	}
	u.Role = authx.NormalizeRole(u.Role)
	return &u, nil
}

func (s *PostgresStore) PhoneExists(ctx context.Context, phone string, excludeUserID int64) (bool, error) {
	phone = strings.TrimSpace(phone)
	if phone == "" {
		return false, nil
	}
	var n int
	err := s.DB.QueryRowContext(ctx, `
SELECT 1 FROM platform_meta.users WHERE phone=$1 AND user_id<>$2 LIMIT 1`, phone, excludeUserID).Scan(&n)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

func (s *PostgresStore) BindPhone(ctx context.Context, userID int64, phone string) (*User, error) {
	phone = strings.TrimSpace(phone)
	if phone == "" {
		_, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET phone='', username_login_disabled=false WHERE user_id=$1`, userID)
		if err != nil {
			return nil, err
		}
		return s.GetByID(ctx, userID)
	}
	ns, err := NormalizePhone(phone)
	if err != nil {
		return nil, err
	}
	exists, err := s.PhoneExists(ctx, ns, userID)
	if err != nil {
		return nil, err
	}
	if exists {
		return nil, fmt.Errorf("该手机号已被绑定")
	}
	res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET phone=$1 WHERE user_id=$2`, ns, userID)
	if err != nil {
		if isUnique(err) {
			return nil, fmt.Errorf("该手机号已被绑定")
		}
		return nil, err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return nil, fmt.Errorf("user not found")
	}
	return s.GetByID(ctx, userID)
}

func (s *PostgresStore) SetUsernameLoginDisabled(ctx context.Context, userID int64, disabled bool) (*User, error) {
	u, err := s.GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if disabled && strings.TrimSpace(u.Phone) == "" {
		return nil, fmt.Errorf("请先绑定手机号，再禁用用户名登录")
	}
	_, err = s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET username_login_disabled=$1 WHERE user_id=$2`, disabled, userID)
	if err != nil {
		return nil, err
	}
	return s.GetByID(ctx, userID)
}

func (s *PostgresStore) JoinTenant(ctx context.Context, userID, tenantID int64, role string, orgUnitID int64) (*User, error) {
	if tenantID <= 0 {
		return nil, fmt.Errorf("invalid tenant")
	}
	role = normalizePlatformRole(role)
	res, err := s.DB.ExecContext(ctx, `
UPDATE platform_meta.users
SET tenant_id=$1, role=$2, status='active', org_unit_id=NULLIF($3,0)
WHERE user_id=$4 AND (tenant_id IS NULL OR status='pending' OR role='pending' OR role='待加入')`,
		tenantID, role, orgUnitID, userID)
	if err != nil {
		return nil, err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		u, gerr := s.GetByID(ctx, userID)
		if gerr != nil {
			return nil, fmt.Errorf("user not found")
		}
		if u.HasTenant() {
			return nil, fmt.Errorf("user already joined a tenant")
		}
		return nil, fmt.Errorf("join tenant failed")
	}
	return s.GetByID(ctx, userID)
}

func (s *PostgresStore) SetOrgUnit(ctx context.Context, userID, tenantID, orgUnitID int64) (*User, error) {
	res, err := s.DB.ExecContext(ctx, `
UPDATE platform_meta.users SET org_unit_id=NULLIF($1,0)
WHERE user_id=$2 AND tenant_id=$3`, orgUnitID, userID, tenantID)
	if err != nil {
		return nil, err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return nil, fmt.Errorf("user not found")
	}
	return s.GetByID(ctx, userID)
}

func (s *PostgresStore) CreateTenantAsOwner(ctx context.Context, userID int64, tenantName string) (*User, error) {
	u, err := s.GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if u.HasTenant() {
		return nil, fmt.Errorf("user already joined a tenant")
	}
	name := strings.TrimSpace(tenantName)
	if name == "" {
		name = u.DisplayName
	}
	if name == "" {
		name = u.Username
	}
	tx, err := s.DB.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer func() { _ = tx.Rollback() }()

	var tenantID int64
	slug := SuggestTenantSlug(name)
	if slug == "" {
		slug = fmt.Sprintf("co%d", time.Now().Unix()%100000)
	}
	ns, nerr := NormalizeTenantSlug(slug)
	if nerr != nil {
		ns = fmt.Sprintf("t%d", time.Now().Unix()%1000000)
	}
	for i := 0; i < 20; i++ {
		try := ns
		if i > 0 {
			try = fmt.Sprintf("%s-%d", ns, i+1)
		}
		err = tx.QueryRowContext(ctx,
			`INSERT INTO platform_meta.tenants(name, slug) VALUES($1,$2) RETURNING tenant_id`, name, try,
		).Scan(&tenantID)
		if err == nil {
			break
		}
		if !isUnique(err) {
			return nil, err
		}
	}
	if err != nil {
		return nil, fmt.Errorf("create tenant slug conflict")
	}
	res, err := tx.ExecContext(ctx, `
UPDATE platform_meta.users
SET tenant_id=$1, role='owner', status='active'
WHERE user_id=$2 AND (tenant_id IS NULL OR status='pending' OR role='pending')`,
		tenantID, userID)
	if err != nil {
		return nil, err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return nil, fmt.Errorf("user already joined a tenant")
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return s.GetByID(ctx, userID)
}

// EnsureDemoUser 开发态确保 demo/demo123 为「演示公司」的公司管理员（不是超管自己）。
func EnsureDemoUser(ctx context.Context, store Store) {
	if store == nil {
		return
	}
	if _, err := store.Login(ctx, "demo", "demo123"); err != nil {
		_, _ = store.EnsureBootstrapOwner(ctx, "demo", "demo123", "演示用户", "演示公司")
	}
	_ = store.RenameTenantIf(ctx, "演示账号", "演示公司")
	_ = store.EnsureTenantSlugs(ctx)
	ensureDevPhone(ctx, store, "demo", "13800000001")
}

// EnsurePlatformAdminUser 确保平台超级管理员 ljk_admin / ljk_admin。
func EnsurePlatformAdminUser(ctx context.Context, store Store) {
	if store == nil {
		return
	}
	if _, err := store.EnsurePlatformAdmin(ctx, "ljk_admin", "ljk_admin", "平台超级管理员"); err != nil {
		fmt.Printf("ensure platform admin ljk_admin: %v\n", err)
	}
	ensureDevPhone(ctx, store, "ljk_admin", "13531041945")
}

func ensureDevPhone(ctx context.Context, store Store, username, phone string) {
	var user *User
	var err error
	switch username {
	case "demo":
		user, err = store.Login(ctx, "demo", "demo123")
	case "ljk_admin":
		user, err = store.Login(ctx, "ljk_admin", "ljk_admin")
	default:
		return
	}
	if err != nil || user == nil {
		return
	}
	want, nerr := NormalizePhone(phone)
	if nerr != nil {
		fmt.Printf("ensure phone for %s: %v\n", username, nerr)
		return
	}
	cur := strings.TrimSpace(user.Phone)
	if cur != want {
		if _, err := store.BindPhone(ctx, user.UserID, want); err != nil {
			fmt.Printf("ensure phone for %s: %v\n", username, err)
			return
		}
	}
	_, _ = store.SetUsernameLoginDisabled(ctx, user.UserID, true)
}

func hashPassword(pw string) (string, error) {
	b, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
	return string(b), err
}

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

func normalizePlatformRole(role string) string {
	n := authx.NormalizeRole(role)
	if authx.ValidPlatformRole(n) {
		return n
	}
	return authx.Role编辑
}
