package agentstore

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"golang.org/x/crypto/bcrypt"

	"aijianzhan/platform/internal/authx"
	"aijianzhan/platform/internal/schema"
)

const (
	StatusActive   = "active"
	StatusPending  = "pending"
	StatusDisabled = "disabled"
)

type Account struct {
	AgentID      int64      `json:"agent_id"`
	TenantID     int64      `json:"tenant_id"`
	Name         string     `json:"name"`
	ClientID     string     `json:"client_id"`
	HostKey      string     `json:"host_key,omitempty"`
	RoleID       int64      `json:"role_id,omitempty"`
	RoleCode     string     `json:"role_code,omitempty"`
	RoleName     string     `json:"role_name,omitempty"`
	Status       string     `json:"status"`
	Perms        []string   `json:"permissions"`
	AppSlugs     []string   `json:"app_slugs"`
	// Z8b：智能体绑定的同步通道 / 线上库（模块与松离线同一目标）
	ChannelID    string     `json:"channel_id,omitempty"`
	OnlineDBID   string     `json:"online_db_id,omitempty"`
	DatabaseName string     `json:"database_name,omitempty"`
	CreatedBy    int64      `json:"created_by"`
	CreatedAt    time.Time  `json:"created_at"`
	LastTokenAt  *time.Time `json:"last_token_at,omitempty"`
}

type CreateInput struct {
	Name         string
	Perms        []string
	AppSlugs     []string
	Status       string // 空则 active
	HostKey      string
	RoleID       int64
	ChannelID    string
	OnlineDBID   string
	DatabaseName string
}

type UpdateInput struct {
	Name         *string
	Status       *string
	Perms        *[]string
	AppSlugs     *[]string
	RoleID       *int64
	ChannelID    *string
	OnlineDBID   *string
	DatabaseName *string
}

type Store interface {
	List(ctx context.Context, tenantID int64) ([]Account, error)
	Get(ctx context.Context, tenantID, agentID int64) (*Account, error)
	Create(ctx context.Context, tenantID, createdBy int64, in CreateInput) (acc *Account, clientSecret string, err error)
	Register(ctx context.Context, tenantID int64, name, hostKey string) (acc *Account, clientSecret string, reused bool, err error)
	Update(ctx context.Context, tenantID, agentID int64, in UpdateInput) (*Account, error)
	RotateSecret(ctx context.Context, tenantID, agentID int64) (clientSecret string, err error)
	Delete(ctx context.Context, tenantID, agentID int64) error
	Authenticate(ctx context.Context, clientID, clientSecret string) (*Account, error)
	TouchToken(ctx context.Context, agentID int64) error
	HasAppAccess(ctx context.Context, agentID int64, slug string) (bool, error)
	// GrantAppSlug 将 slug 写入智能体可访问模块（幂等）。新建发布时自动授权用。
	GrantAppSlug(ctx context.Context, agentID int64, slug string) error
	// FindByHostKey 按宿主机 key 查找（跨租户，Z13 绑定用）。
	FindByHostKey(ctx context.Context, hostKey string) (*Account, error)
	// AttachSyncBind 写入同步落点；可改挂租户并激活（Z12/Z13）。
	AttachSyncBind(ctx context.Context, agentID, tenantID int64, channelID, onlineDBID, databaseName string, activate bool) (*Account, error)
}

type memAcc struct {
	Account
	SecretHash string
}

type MemoryStore struct {
	mu    sync.Mutex
	seq   int64
	byID  map[int64]*memAcc
	byCID map[string]*memAcc
}

func NewMemoryStore() *MemoryStore {
	return &MemoryStore{byID: map[int64]*memAcc{}, byCID: map[string]*memAcc{}}
}

func (s *MemoryStore) List(_ context.Context, tenantID int64) ([]Account, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	out := []Account{}
	for _, a := range s.byID {
		if a.TenantID == tenantID {
			out = append(out, cloneAcc(&a.Account))
		}
	}
	return out, nil
}

func (s *MemoryStore) Get(_ context.Context, tenantID, agentID int64) (*Account, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	a, ok := s.byID[agentID]
	if !ok || a.TenantID != tenantID {
		return nil, fmt.Errorf("agent not found")
	}
	cp := cloneAcc(&a.Account)
	return &cp, nil
}

func (s *MemoryStore) Create(_ context.Context, tenantID, createdBy int64, in CreateInput) (*Account, string, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.createLocked(tenantID, createdBy, in)
}

func (s *MemoryStore) Register(_ context.Context, tenantID int64, name, hostKey string) (*Account, string, bool, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	hostKey = strings.TrimSpace(hostKey)
	if hostKey != "" {
		for _, a := range s.byID {
			if a.HostKey != hostKey {
				continue
			}
			// Z28：已有任意租户下 active+sync_bound → 禁止另起 pending
			if a.Status == StatusActive && strings.TrimSpace(a.ChannelID) != "" && strings.TrimSpace(a.OnlineDBID) != "" {
				if a.TenantID != tenantID {
					return nil, "", false, fmt.Errorf("host_key 已同步绑定其它公司（client_id=%s）；请使用同步绑定凭证换票，勿再 SelfRegister", a.ClientID)
				}
				secret := randomHex(24)
				hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
				if err != nil {
					return nil, "", false, err
				}
				a.SecretHash = string(hash)
				if n := strings.TrimSpace(name); n != "" {
					a.Name = n
				}
				cp := cloneAcc(&a.Account)
				return &cp, secret, true, nil
			}
			if a.TenantID != tenantID {
				continue
			}
			if a.Status == StatusPending {
				secret := randomHex(24)
				hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
				if err != nil {
					return nil, "", false, err
				}
				a.SecretHash = string(hash)
				if n := strings.TrimSpace(name); n != "" {
					a.Name = n
				}
				cp := cloneAcc(&a.Account)
				return &cp, secret, true, nil
			}
			return nil, "", false, fmt.Errorf("host already registered as %s (status=%s)", a.ClientID, a.Status)
		}
	}
	acc, secret, err := s.createLocked(tenantID, 0, CreateInput{
		Name:    name,
		Status:  StatusPending,
		HostKey: hostKey,
	})
	return acc, secret, false, err
}

func (s *MemoryStore) createLocked(tenantID, createdBy int64, in CreateInput) (*Account, string, error) {
	cid := "agt_" + randomHex(8)
	secret := randomHex(24)
	hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
	if err != nil {
		return nil, "", err
	}
	st := strings.TrimSpace(in.Status)
	if st == "" {
		st = StatusActive
	}
	if !validStatus(st) {
		return nil, "", fmt.Errorf("invalid status")
	}
	s.seq++
	a := &memAcc{
		Account: Account{
			AgentID:      s.seq,
			TenantID:     tenantID,
			Name:         strings.TrimSpace(in.Name),
			ClientID:     cid,
			HostKey:      strings.TrimSpace(in.HostKey),
			RoleID:       in.RoleID,
			Status:       st,
			Perms:        authx.NormalizePerms(uniq(in.Perms)),
			AppSlugs:     uniq(in.AppSlugs),
			ChannelID:    strings.TrimSpace(in.ChannelID),
			OnlineDBID:   strings.TrimSpace(in.OnlineDBID),
			DatabaseName: strings.TrimSpace(in.DatabaseName),
			CreatedBy:    createdBy,
			CreatedAt:    time.Now().UTC(),
		},
		SecretHash: string(hash),
	}
	if a.Name == "" {
		a.Name = cid
	}
	s.byID[a.AgentID] = a
	s.byCID[a.ClientID] = a
	cp := cloneAcc(&a.Account)
	return &cp, secret, nil
}

func (s *MemoryStore) Update(_ context.Context, tenantID, agentID int64, in UpdateInput) (*Account, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	a, ok := s.byID[agentID]
	if !ok || a.TenantID != tenantID {
		return nil, fmt.Errorf("agent not found")
	}
	if in.Name != nil {
		a.Name = strings.TrimSpace(*in.Name)
	}
	if in.Status != nil {
		st := strings.TrimSpace(*in.Status)
		if !validStatus(st) {
			return nil, fmt.Errorf("invalid status")
		}
		a.Status = st
	}
	if in.Perms != nil {
		a.Perms = uniq(*in.Perms)
	}
	if in.AppSlugs != nil {
		a.AppSlugs = uniq(*in.AppSlugs)
	}
	if in.RoleID != nil {
		a.RoleID = *in.RoleID
	}
	if in.ChannelID != nil {
		a.ChannelID = strings.TrimSpace(*in.ChannelID)
	}
	if in.OnlineDBID != nil {
		a.OnlineDBID = strings.TrimSpace(*in.OnlineDBID)
	}
	if in.DatabaseName != nil {
		a.DatabaseName = schema.SafeSyncDatabaseName(*in.DatabaseName, agentID, 0)
	}
	cp := cloneAcc(&a.Account)
	return &cp, nil
}

func (s *MemoryStore) RotateSecret(_ context.Context, tenantID, agentID int64) (string, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	a, ok := s.byID[agentID]
	if !ok || a.TenantID != tenantID {
		return "", fmt.Errorf("agent not found")
	}
	secret := randomHex(24)
	hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}
	a.SecretHash = string(hash)
	return secret, nil
}

func (s *MemoryStore) Delete(_ context.Context, tenantID, agentID int64) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	a, ok := s.byID[agentID]
	if !ok || a.TenantID != tenantID {
		return fmt.Errorf("agent not found")
	}
	delete(s.byCID, a.ClientID)
	delete(s.byID, agentID)
	return nil
}

func (s *MemoryStore) Authenticate(_ context.Context, clientID, clientSecret string) (*Account, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	a, ok := s.byCID[clientID]
	if !ok {
		return nil, fmt.Errorf("invalid client credentials")
	}
	if bcrypt.CompareHashAndPassword([]byte(a.SecretHash), []byte(clientSecret)) != nil {
		return nil, fmt.Errorf("invalid client credentials")
	}
	switch a.Status {
	case StatusPending:
		return nil, fmt.Errorf("agent pending approval")
	case StatusDisabled:
		return nil, fmt.Errorf("agent disabled")
	case StatusActive:
		// ok
	default:
		return nil, fmt.Errorf("agent not active")
	}
	cp := cloneAcc(&a.Account)
	return &cp, nil
}

func (s *MemoryStore) TouchToken(_ context.Context, agentID int64) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if a, ok := s.byID[agentID]; ok {
		now := time.Now().UTC()
		a.LastTokenAt = &now
	}
	return nil
}

func (s *MemoryStore) HasAppAccess(_ context.Context, agentID int64, slug string) (bool, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	a, ok := s.byID[agentID]
	if !ok {
		return false, fmt.Errorf("agent not found")
	}
	// 未配置可访问模块 = 不限制，智能体可自由发布/访问自建模块
	if len(a.AppSlugs) == 0 {
		return true, nil
	}
	for _, s0 := range a.AppSlugs {
		if s0 == "*" || s0 == slug {
			return true, nil
		}
	}
	return false, nil
}

func (s *MemoryStore) GrantAppSlug(_ context.Context, agentID int64, slug string) error {
	slug = strings.TrimSpace(slug)
	if slug == "" {
		return fmt.Errorf("empty slug")
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	a, ok := s.byID[agentID]
	if !ok {
		return fmt.Errorf("agent not found")
	}
	for _, s0 := range a.AppSlugs {
		if s0 == slug {
			return nil
		}
	}
	a.AppSlugs = append(a.AppSlugs, slug)
	return nil
}

func (s *MemoryStore) FindByHostKey(_ context.Context, hostKey string) (*Account, error) {
	hostKey = strings.TrimSpace(hostKey)
	if hostKey == "" {
		return nil, fmt.Errorf("host_key required")
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	for _, a := range s.byID {
		if a.HostKey == hostKey {
			cp := cloneAcc(&a.Account)
			return &cp, nil
		}
	}
	return nil, fmt.Errorf("agent not found")
}

func (s *MemoryStore) AttachSyncBind(_ context.Context, agentID, tenantID int64, channelID, onlineDBID, databaseName string, activate bool) (*Account, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	a, ok := s.byID[agentID]
	if !ok {
		return nil, fmt.Errorf("agent not found")
	}
	if tenantID > 0 {
		a.TenantID = tenantID
	}
	a.ChannelID = strings.TrimSpace(channelID)
	a.OnlineDBID = strings.TrimSpace(onlineDBID)
	// Z35：禁止中文 DisplayName 等非法串写入物理库名
	a.DatabaseName = schema.SafeSyncDatabaseName(databaseName, agentID, 0)
	if activate {
		a.Status = StatusActive
	}
	cp := cloneAcc(&a.Account)
	return &cp, nil
}

type PostgresStore struct {
	DB *sql.DB
}

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

func (s *PostgresStore) List(ctx context.Context, tenantID int64) ([]Account, error) {
	rows, err := s.DB.QueryContext(ctx, `
SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0),
       COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''),
       status, created_by, created_at, last_token_at
FROM platform_meta.agent_accounts WHERE tenant_id=$1
ORDER BY CASE status WHEN 'pending' THEN 0 WHEN 'active' THEN 1 ELSE 2 END, agent_id`, tenantID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Account
	for rows.Next() {
		var a Account
		var last sql.NullTime
		if err := rows.Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID,
			&a.ChannelID, &a.OnlineDBID, &a.DatabaseName,
			&a.Status, &a.CreatedBy, &a.CreatedAt, &last); err != nil {
			return nil, err
		}
		if last.Valid {
			t := last.Time.UTC()
			a.LastTokenAt = &t
		}
		perms, slugs, err := s.loadKids(ctx, a.AgentID)
		if err != nil {
			return nil, err
		}
		a.Perms, a.AppSlugs = perms, slugs
		out = append(out, a)
	}
	return out, rows.Err()
}

func (s *PostgresStore) Get(ctx context.Context, tenantID, agentID int64) (*Account, error) {
	var a Account
	var last sql.NullTime
	err := s.DB.QueryRowContext(ctx, `
SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0),
       COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''),
       status, created_by, created_at, last_token_at
FROM platform_meta.agent_accounts WHERE agent_id=$1 AND tenant_id=$2`, agentID, tenantID,
	).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID,
		&a.ChannelID, &a.OnlineDBID, &a.DatabaseName,
		&a.Status, &a.CreatedBy, &a.CreatedAt, &last)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, fmt.Errorf("agent not found")
	}
	if err != nil {
		return nil, err
	}
	if last.Valid {
		t := last.Time.UTC()
		a.LastTokenAt = &t
	}
	perms, slugs, err := s.loadKids(ctx, a.AgentID)
	if err != nil {
		return nil, err
	}
	a.Perms, a.AppSlugs = perms, slugs
	return &a, nil
}

func (s *PostgresStore) Create(ctx context.Context, tenantID, createdBy int64, in CreateInput) (*Account, string, error) {
	return s.insert(ctx, tenantID, createdBy, in)
}

func (s *PostgresStore) Register(ctx context.Context, tenantID int64, name, hostKey string) (*Account, string, bool, error) {
	hostKey = strings.TrimSpace(hostKey)
	if hostKey != "" {
		// Z28：先按 host_key 全局查找，避免已 sync_bound 时另起 pending
		if existing, err := s.FindByHostKey(ctx, hostKey); err == nil && existing != nil {
			syncBound := strings.TrimSpace(existing.ChannelID) != "" && strings.TrimSpace(existing.OnlineDBID) != ""
			if existing.Status == StatusActive && syncBound {
				if existing.TenantID != tenantID {
					return nil, "", false, fmt.Errorf("host_key 已同步绑定其它公司（client_id=%s）；请使用同步绑定凭证换票，勿再 SelfRegister", existing.ClientID)
				}
				secret, err := s.RotateSecret(ctx, tenantID, existing.AgentID)
				if err != nil {
					return nil, "", false, err
				}
				if n := strings.TrimSpace(name); n != "" {
					_, _ = s.Update(ctx, tenantID, existing.AgentID, UpdateInput{Name: &n})
				}
				acc, err := s.Get(ctx, tenantID, existing.AgentID)
				return acc, secret, true, err
			}
			if existing.TenantID == tenantID && existing.Status == StatusPending {
				secret, err := s.RotateSecret(ctx, tenantID, existing.AgentID)
				if err != nil {
					return nil, "", false, err
				}
				if n := strings.TrimSpace(name); n != "" {
					_, _ = s.Update(ctx, tenantID, existing.AgentID, UpdateInput{Name: &n})
				}
				acc, err := s.Get(ctx, tenantID, existing.AgentID)
				return acc, secret, true, err
			}
			if existing.TenantID == tenantID {
				return nil, "", false, fmt.Errorf("host already registered as %s (status=%s)", existing.ClientID, existing.Status)
			}
		} else if err != nil {
			// FindByHostKey 未找到 → 继续新建 pending
			if !strings.Contains(err.Error(), "not found") {
				return nil, "", false, err
			}
		}
	}
	acc, secret, err := s.insert(ctx, tenantID, 0, CreateInput{
		Name:    name,
		Status:  StatusPending,
		HostKey: hostKey,
	})
	return acc, secret, false, err
}

func (s *PostgresStore) insert(ctx context.Context, tenantID, createdBy int64, in CreateInput) (*Account, string, error) {
	cid := "agt_" + randomHex(8)
	secret := randomHex(24)
	hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
	if err != nil {
		return nil, "", err
	}
	name := strings.TrimSpace(in.Name)
	if name == "" {
		name = cid
	}
	st := strings.TrimSpace(in.Status)
	if st == "" {
		st = StatusActive
	}
	if !validStatus(st) {
		return nil, "", fmt.Errorf("invalid status")
	}
	var a Account
	err = s.DB.QueryRowContext(ctx, `
INSERT INTO platform_meta.agent_accounts(tenant_id, name, client_id, client_secret_hash, status, created_by, host_key, role_id, channel_id, online_db_id, database_name)
VALUES($1,$2,$3,$4,$5,$6,$7,NULLIF($8,0),$9,$10,$11)
RETURNING agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0),
          COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''),
          status, created_by, created_at`,
		tenantID, name, cid, string(hash), st, createdBy, strings.TrimSpace(in.HostKey), in.RoleID,
		strings.TrimSpace(in.ChannelID), strings.TrimSpace(in.OnlineDBID), strings.TrimSpace(in.DatabaseName),
	).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID,
		&a.ChannelID, &a.OnlineDBID, &a.DatabaseName,
		&a.Status, &a.CreatedBy, &a.CreatedAt)
	if err != nil {
		return nil, "", err
	}
	perms, slugs := uniq(in.Perms), uniq(in.AppSlugs)
	if err := s.replaceKids(ctx, a.AgentID, perms, slugs); err != nil {
		return nil, "", err
	}
	a.Perms, a.AppSlugs = perms, slugs
	return &a, secret, nil
}

func (s *PostgresStore) Update(ctx context.Context, tenantID, agentID int64, in UpdateInput) (*Account, error) {
	a, err := s.Get(ctx, tenantID, agentID)
	if err != nil {
		return nil, err
	}
	name, status := a.Name, a.Status
	roleID := a.RoleID
	channelID, onlineDBID, dbName := a.ChannelID, a.OnlineDBID, a.DatabaseName
	if in.Name != nil {
		name = strings.TrimSpace(*in.Name)
	}
	if in.Status != nil {
		status = strings.TrimSpace(*in.Status)
		if !validStatus(status) {
			return nil, fmt.Errorf("invalid status")
		}
	}
	if in.RoleID != nil {
		roleID = *in.RoleID
	}
	if in.ChannelID != nil {
		channelID = strings.TrimSpace(*in.ChannelID)
	}
	if in.OnlineDBID != nil {
		onlineDBID = strings.TrimSpace(*in.OnlineDBID)
	}
	if in.DatabaseName != nil {
		dbName = schema.SafeSyncDatabaseName(*in.DatabaseName, agentID, 0)
	}
	if _, err := s.DB.ExecContext(ctx, `
UPDATE platform_meta.agent_accounts SET name=$1, status=$2, role_id=NULLIF($3,0),
  channel_id=$4, online_db_id=$5, database_name=$6
WHERE agent_id=$7 AND tenant_id=$8`,
		name, status, roleID, channelID, onlineDBID, dbName, agentID, tenantID); err != nil {
		return nil, err
	}
	perms, slugs := a.Perms, a.AppSlugs
	if in.Perms != nil {
		perms = uniq(*in.Perms)
	}
	if in.AppSlugs != nil {
		slugs = uniq(*in.AppSlugs)
	}
	if err := s.replaceKids(ctx, agentID, perms, slugs); err != nil {
		return nil, err
	}
	return s.Get(ctx, tenantID, agentID)
}

func (s *PostgresStore) RotateSecret(ctx context.Context, tenantID, agentID int64) (string, error) {
	if _, err := s.Get(ctx, tenantID, agentID); err != nil {
		return "", err
	}
	secret := randomHex(24)
	hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}
	_, err = s.DB.ExecContext(ctx, `
UPDATE platform_meta.agent_accounts SET client_secret_hash=$1 WHERE agent_id=$2 AND tenant_id=$3`,
		string(hash), agentID, tenantID)
	return secret, err
}

func (s *PostgresStore) Delete(ctx context.Context, tenantID, agentID int64) error {
	res, err := s.DB.ExecContext(ctx, `
DELETE FROM platform_meta.agent_accounts WHERE agent_id=$1 AND tenant_id=$2`, agentID, tenantID)
	if err != nil {
		return err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return fmt.Errorf("agent not found")
	}
	return nil
}

func (s *PostgresStore) Authenticate(ctx context.Context, clientID, clientSecret string) (*Account, error) {
	var a Account
	var hash string
	var last sql.NullTime
	err := s.DB.QueryRowContext(ctx, `
SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0),
       COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''),
       client_secret_hash, status, created_by, created_at, last_token_at
FROM platform_meta.agent_accounts WHERE client_id=$1`, clientID,
	).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID,
		&a.ChannelID, &a.OnlineDBID, &a.DatabaseName,
		&hash, &a.Status, &a.CreatedBy, &a.CreatedAt, &last)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, fmt.Errorf("invalid client credentials")
	}
	if err != nil {
		return nil, err
	}
	if bcrypt.CompareHashAndPassword([]byte(hash), []byte(clientSecret)) != nil {
		return nil, fmt.Errorf("invalid client credentials")
	}
	switch a.Status {
	case StatusPending:
		return nil, fmt.Errorf("agent pending approval")
	case StatusDisabled:
		return nil, fmt.Errorf("agent disabled")
	case StatusActive:
	default:
		return nil, fmt.Errorf("agent not active")
	}
	if last.Valid {
		t := last.Time.UTC()
		a.LastTokenAt = &t
	}
	perms, slugs, err := s.loadKids(ctx, a.AgentID)
	if err != nil {
		return nil, err
	}
	a.Perms, a.AppSlugs = perms, slugs
	return &a, nil
}

func (s *PostgresStore) TouchToken(ctx context.Context, agentID int64) error {
	_, err := s.DB.ExecContext(ctx, `
UPDATE platform_meta.agent_accounts SET last_token_at=now() WHERE agent_id=$1`, agentID)
	return err
}

func (s *PostgresStore) HasAppAccess(ctx context.Context, agentID int64, slug string) (bool, error) {
	var total int
	if err := s.DB.QueryRowContext(ctx, `
SELECT COUNT(1) FROM platform_meta.agent_app_grants WHERE agent_id=$1`, agentID).Scan(&total); err != nil {
		return false, err
	}
	// 未配置可访问模块 = 不限制，可自由发布自己的模块
	if total == 0 {
		return true, nil
	}
	var n int
	err := s.DB.QueryRowContext(ctx, `
SELECT COUNT(1) FROM platform_meta.agent_app_grants
WHERE agent_id=$1 AND (slug=$2 OR slug='*')`, agentID, slug).Scan(&n)
	return n > 0, err
}

func (s *PostgresStore) GrantAppSlug(ctx context.Context, agentID int64, slug string) error {
	slug = strings.TrimSpace(slug)
	if slug == "" {
		return fmt.Errorf("empty slug")
	}
	ok, err := s.HasAppAccess(ctx, agentID, slug)
	if err != nil {
		return err
	}
	if ok {
		return nil
	}
	_, err = s.DB.ExecContext(ctx, `
INSERT INTO platform_meta.agent_app_grants(agent_id, slug) VALUES($1,$2)`, agentID, slug)
	return err
}

func (s *PostgresStore) FindByHostKey(ctx context.Context, hostKey string) (*Account, error) {
	hostKey = strings.TrimSpace(hostKey)
	if hostKey == "" {
		return nil, fmt.Errorf("host_key required")
	}
	var a Account
	var last sql.NullTime
	err := s.DB.QueryRowContext(ctx, `
SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0),
       COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''),
       status, created_by, created_at, last_token_at
FROM platform_meta.agent_accounts WHERE host_key=$1
ORDER BY agent_id DESC LIMIT 1`, hostKey,
	).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID,
		&a.ChannelID, &a.OnlineDBID, &a.DatabaseName,
		&a.Status, &a.CreatedBy, &a.CreatedAt, &last)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, fmt.Errorf("agent not found")
	}
	if err != nil {
		return nil, err
	}
	if last.Valid {
		t := last.Time
		a.LastTokenAt = &t
	}
	perms, slugs, err := s.loadKids(ctx, a.AgentID)
	if err != nil {
		return nil, err
	}
	a.Perms, a.AppSlugs = perms, slugs
	return &a, nil
}

func (s *PostgresStore) AttachSyncBind(ctx context.Context, agentID, tenantID int64, channelID, onlineDBID, databaseName string, activate bool) (*Account, error) {
	statusSQL := ""
	safeDB := schema.SafeSyncDatabaseName(databaseName, agentID, 0)
	args := []any{tenantID, strings.TrimSpace(channelID), strings.TrimSpace(onlineDBID), safeDB, agentID}
	if activate {
		statusSQL = ", status='active'"
	}
	res, err := s.DB.ExecContext(ctx, `
UPDATE platform_meta.agent_accounts SET tenant_id=$1, channel_id=$2, online_db_id=$3, database_name=$4`+statusSQL+`
WHERE agent_id=$5`, args...)
	if err != nil {
		return nil, err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return nil, fmt.Errorf("agent not found")
	}
	return s.Get(ctx, tenantID, agentID)
}

func (s *PostgresStore) loadKids(ctx context.Context, agentID int64) ([]string, []string, error) {
	prows, err := s.DB.QueryContext(ctx, `SELECT perm FROM platform_meta.agent_permissions WHERE agent_id=$1`, agentID)
	if err != nil {
		return nil, nil, err
	}
	defer prows.Close()
	var perms []string
	for prows.Next() {
		var p string
		if err := prows.Scan(&p); err != nil {
			return nil, nil, err
		}
		perms = append(perms, p)
	}
	srows, err := s.DB.QueryContext(ctx, `SELECT slug FROM platform_meta.agent_app_grants WHERE agent_id=$1`, agentID)
	if err != nil {
		return nil, nil, err
	}
	defer srows.Close()
	var slugs []string
	for srows.Next() {
		var slug string
		if err := srows.Scan(&slug); err != nil {
			return nil, nil, err
		}
		slugs = append(slugs, slug)
	}
	return authx.NormalizePerms(perms), slugs, nil
}

func (s *PostgresStore) replaceKids(ctx context.Context, agentID int64, perms, slugs []string) error {
	tx, err := s.DB.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()
	if _, err := tx.ExecContext(ctx, `DELETE FROM platform_meta.agent_permissions WHERE agent_id=$1`, agentID); err != nil {
		return err
	}
	if _, err := tx.ExecContext(ctx, `DELETE FROM platform_meta.agent_app_grants WHERE agent_id=$1`, agentID); err != nil {
		return err
	}
	for _, p := range authx.NormalizePerms(perms) {
		if _, err := tx.ExecContext(ctx, `INSERT INTO platform_meta.agent_permissions(agent_id, perm) VALUES($1,$2)`, agentID, p); err != nil {
			return err
		}
	}
	for _, slug := range slugs {
		if _, err := tx.ExecContext(ctx, `INSERT INTO platform_meta.agent_app_grants(agent_id, slug) VALUES($1,$2)`, agentID, slug); err != nil {
			return err
		}
	}
	return tx.Commit()
}

func validStatus(st string) bool {
	return st == StatusActive || st == StatusPending || st == StatusDisabled
}

func cloneAcc(a *Account) Account {
	cp := *a
	cp.Perms = append([]string{}, a.Perms...)
	cp.AppSlugs = append([]string{}, a.AppSlugs...)
	return cp
}

func uniq(in []string) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, len(in))
	for _, s := range in {
		s = strings.TrimSpace(s)
		if s == "" {
			continue
		}
		if _, ok := seen[s]; ok {
			continue
		}
		seen[s] = struct{}{}
		out = append(out, s)
	}
	return out
}

func randomHex(n int) string {
	b := make([]byte, n)
	_, _ = rand.Read(b)
	return hex.EncodeToString(b)
}
