// Package user 内的 service.go 实装 Service 接口：把 auth 工具（密码哈希、
// session token）和 Repository（持久层）粘合为一组面向 transport 层的应用
// 服务方法。
//
// 设计取舍：
//   - 不区分 "邮箱不存在" 与 "密码错误"：两条路径都返回同一个 CodeUnauthorized
//     错误，避免登录入口被用作邮箱存在性探测。
//   - ResolveSession 把 "未授权" 与 "未找到" 都翻译成 (uuid.Nil, false, nil)，
//     符合 transport/http/middleware.SessionResolver 接口约定（token 无效不算
//     系统错误，由中间件统一返回 401）。
//   - Bootstrap 仅在 DB 中尚无任何用户时落地新管理员；已有用户直接返回
//     (nil, nil)，使重复执行 bootstrap 流程不会覆盖现网管理员。
//   - Login 把 ip 透传给 audit recorder（IP 解析在 audit 包内完成）。
//   - now 字段以 func() time.Time 而非直接调用 time.Now 持有，方便后续测试
//     冻结时间（首版 5 个测试还用不到，但接口先留）。
package user

import (
	"context"
	"time"

	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/audit"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)

// sessionTTL 是单条会话的有效期。7 天与多数 Web 控制台默认值一致；上层未
// 暴露续期能力时，到期后客户端需重新登录。
const sessionTTL = 7 * 24 * time.Hour

// service 是 Service 接口的默认实现。字段保持小写以禁止外部直接构造，
// 调用方必须通过 NewService 注入 Repository。
type service struct {
	repo     Repository
	audit    audit.Recorder
	now      func() time.Time
	throttle *LoginThrottle // 登录暴力破解节流；默认 disabled，装配期可回填
}

// NewService 用给定的 Repository 和 audit.Recorder 构造 Service 实现。
// recorder 可为 nil（审计是 best-effort，nil 时直接跳过写审计）。
// 返回 Service 接口而非 *service，以便日后扩展实现（如增加缓存层装饰器）
// 时不破坏调用方代码。默认装一个 disabled 的 LoginThrottle（无节流），
// 装配期通过 SetLoginThrottle 注入启用的实例。
func NewService(repo Repository, recorder audit.Recorder) Service {
	return &service{
		repo:     repo,
		audit:    recorder,
		now:      time.Now,
		throttle: NewLoginThrottle(LoginThrottleConfig{}, nil),
	}
}

// SetLoginThrottle 注入登录节流器（装配期回填）。传 nil 时退回 disabled。
// 因 Service 是接口，调用方需类型断言到该 setter（与项目其它装配期回填一致）。
func (s *service) SetLoginThrottle(t *LoginThrottle) {
	if t == nil {
		t = NewLoginThrottle(LoginThrottleConfig{}, nil)
	}
	s.throttle = t
}

// LoginThrottleSetter 是装配期回填登录节流器的窄接口（app.go 用类型断言取得）。
type LoginThrottleSetter interface {
	SetLoginThrottle(t *LoginThrottle)
}

// Login 校验邮箱与密码，成功后签发新的会话 token。
// 任何与凭据相关的失败（用户不存在、密码不匹配）都统一返回 CodeUnauthorized，
// 不向调用方泄露 "邮箱是否存在" 这一侧信道；只有底层故障（哈希解析、
// token 生成、写库失败等）会返回带详细 cause 的非授权类错误。
func (s *service) Login(ctx context.Context, email, password, ip string) (string, *User, error) {
	// 暴力破解节流：在任何 DB 查询 / 密码哈希之前判定，超限直接 429，避免攻击者
	// 持续消耗 bcrypt CPU。key = ip + lowercase(email)，未知邮箱与已知邮箱走同一
	// 计数路径，不引入邮箱枚举侧信道。
	if allowed, _ := s.throttle.Allow(ip, email); !allowed {
		return "", nil, errs.New(errs.CodeRateLimited, "登录尝试过于频繁，请稍后再试")
	}

	u, err := s.repo.GetUserByEmail(ctx, email)
	if err != nil {
		// 不区分 "邮箱不存在" 与 "密码错误"，统一报 unauthorized。失败计数。
		s.throttle.RecordFailure(ip, email)
		return "", nil, errs.New(errs.CodeUnauthorized, "邮箱或密码错误")
	}
	ok, err := VerifyPassword(password, u.PasswordHash)
	if err != nil {
		return "", nil, errs.Wrap(err, errs.CodeInternal, "verify password")
	}
	if !ok {
		s.throttle.RecordFailure(ip, email)
		return "", nil, errs.New(errs.CodeUnauthorized, "邮箱或密码错误")
	}
	// 凭据正确：清零该 key 的失败计数。
	s.throttle.Reset(ip, email)

	tok, hash, err := NewSessionToken()
	if err != nil {
		return "", nil, errs.Wrap(err, errs.CodeInternal, "new token")
	}
	sess := &Session{
		ID:        uuid.New(),
		UserID:    u.ID,
		TokenHash: hash,
		ExpiresAt: s.now().Add(sessionTTL),
	}
	if err := s.repo.CreateSession(ctx, sess); err != nil {
		return "", nil, err
	}
	// 审计是 best-effort：_ = 吞掉错误，审计落库失败不能导致登录失败。
	// 用 context.WithoutCancel 派生：HTTP handler 在响应返回后 r.Context() 会
	// 被取消，但审计写入必须完成（漏行比写错更危险）。timeout 由调用方在
	// ctx 上不再控制，audit recorder 自身需保证不无限阻塞。
	if s.audit != nil {
		uid := u.ID
		_ = s.audit.Record(context.WithoutCancel(ctx), audit.Entry{
			UserID:     &uid,
			Action:     "user.login",
			TargetType: "user",
			TargetID:   u.ID.String(),
			IP:         ip,
		})
	}
	return tok, u, nil
}

// Logout 删除给定 token 对应的会话。pgRepo.DeleteSessionByTokenHash 对不存在
// 的行不返错，因此本方法天然幂等：客户端重复登出或登出未知 token 都不会
// 报错。
// 先 ResolveSession 拿 uid 是为了给 audit Entry 填充 UserID；ResolveSession
// 失败（如 token 已过期/无效）时依然继续删除会话（保持幂等），只是 uid 为
// uuid.Nil，此时跳过写审计。
func (s *service) Logout(ctx context.Context, token string) error {
	uid, _, _ := s.ResolveSession(ctx, token)
	if err := s.repo.DeleteSessionByTokenHash(ctx, HashSessionToken(token)); err != nil {
		return err
	}
	// 审计是 best-effort：_ = 吞掉错误；uid == uuid.Nil 表示 token 无效，跳过。
	// 用 context.WithoutCancel 派生：HTTP handler 在响应返回后 r.Context() 会
	// 被取消，但审计写入必须完成（漏行比写错更危险）。timeout 由调用方在
	// ctx 上不再控制，audit recorder 自身需保证不无限阻塞。
	if s.audit != nil && uid != uuid.Nil {
		u := uid
		_ = s.audit.Record(context.WithoutCancel(ctx), audit.Entry{
			UserID:     &u,
			Action:     "user.logout",
			TargetType: "user",
			TargetID:   uid.String(),
		})
	}
	return nil
}

// ResolveSession 把 bearer token 翻译为已认证用户 ID。空 token 直接视作匿名
// 请求；存在但未知/过期的 token 也归一为 (uuid.Nil, false, nil)，由调用方
// （通常是 Auth 中间件）按 401 处理。只有真正的底层故障才会返回非 nil
// error，从而在系统异常时不掩盖问题。
func (s *service) ResolveSession(ctx context.Context, token string) (uuid.UUID, bool, error) {
	if token == "" {
		return uuid.Nil, false, nil
	}
	sess, err := s.repo.GetSessionByTokenHash(ctx, HashSessionToken(token))
	if err != nil {
		ae, ok := errs.As(err)
		if ok && (ae.Code == errs.CodeUnauthorized || ae.Code == errs.CodeNotFound) {
			return uuid.Nil, false, nil
		}
		return uuid.Nil, false, err
	}
	// touch 是 best-effort：失败也不影响本次解析结果，留 _ 显式忽略。
	_ = s.repo.TouchSession(ctx, sess.TokenHash)
	return sess.UserID, true, nil
}

// Get 按主键加载用户。错误透传 Repository 层（NotFound 已在那里翻译完成）。
func (s *service) Get(ctx context.Context, id uuid.UUID) (*User, error) {
	return s.repo.GetUserByID(ctx, id)
}

// Bootstrap 在系统首次启动时创建初始管理员账号。已有任意用户时返回
// (nil, nil) 表示 "已无需 bootstrap"，调用方据此判断是否要继续打印初始
// 凭据。密码长度由 HashPassword 校验；若传入太短的密码，错误码归一为
// CodeInvalidInput。
func (s *service) Bootstrap(ctx context.Context, email, password string) (*User, error) {
	count, err := s.repo.CountUsers(ctx)
	if err != nil {
		return nil, err
	}
	if count > 0 {
		// 已有用户，bootstrap 仅在空 DB 时生效。
		return nil, nil
	}
	hash, err := HashPassword(password)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInvalidInput, "bootstrap password")
	}
	u := &User{
		ID:           uuid.New(),
		Email:        email,
		PasswordHash: hash,
		DisplayName:  "Admin",
		IsAdmin:      true,
	}
	return s.repo.CreateUser(ctx, u)
}

// ListAdmins 返回所有管理员用户，供内部模块（如 jobs runner）使用。
func (s *service) ListAdmins(ctx context.Context) ([]*User, error) {
	return s.repo.ListAdmins(ctx)
}

// tempPasswordLen 是管理员重置密码时生成的临时明文长度（base64 字符数）。
// 16 字符远超 minPasswordLen，且使用与会话 token 相同的强随机源。
const tempPasswordLen = 16

// ListUsers 返回全部用户，供管理后台展示。调用方负责 admin 鉴权。
func (s *service) ListUsers(ctx context.Context) ([]*User, error) {
	return s.repo.ListUsers(ctx)
}

// CreateUser 由管理员创建新账号。密码长度由 HashPassword 校验；email 冲突时
// Repository 返回 CodeConflict。
func (s *service) CreateUser(ctx context.Context, in CreateUserInput) (*User, error) {
	hash, err := HashPassword(in.Password)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInvalidInput, "password")
	}
	u := &User{
		ID:           uuid.New(),
		Email:        in.Email,
		PasswordHash: hash,
		DisplayName:  in.DisplayName,
		IsAdmin:      in.IsAdmin,
	}
	out, err := s.repo.CreateUser(ctx, u)
	if err != nil {
		return nil, err
	}
	s.record(ctx, &out.ID, "user.create", out.ID.String())
	return out, nil
}

// UpdateProfile 修改用户 display_name。
func (s *service) UpdateProfile(ctx context.Context, id uuid.UUID, displayName string) (*User, error) {
	out, err := s.repo.UpdateProfile(ctx, id, displayName)
	if err != nil {
		return nil, err
	}
	s.record(ctx, &id, "user.update_profile", id.String())
	return out, nil
}

// SetAdmin 调整用户管理员标志。两条护栏：actor 不能降级自己（防误操作把自己
// 锁在门外）；不能降级最后一个启用管理员（防系统失去管理员）。
func (s *service) SetAdmin(ctx context.Context, actor, target uuid.UUID, isAdmin bool) (*User, error) {
	if !isAdmin {
		if actor == target {
			return nil, errs.New(errs.CodeConflict, "不能取消自己的管理员权限")
		}
		if err := s.guardLastAdmin(ctx, target); err != nil {
			return nil, err
		}
	}
	out, err := s.repo.SetAdmin(ctx, target, isAdmin)
	if err != nil {
		return nil, err
	}
	s.record(ctx, &actor, "user.set_admin", target.String())
	return out, nil
}

// SetEnabled 启用/禁用用户。禁用时撤销其全部会话使其立即下线，并施加与
// SetAdmin 同样的自锁/最后管理员护栏。
func (s *service) SetEnabled(ctx context.Context, actor, target uuid.UUID, enabled bool) (*User, error) {
	if !enabled {
		if actor == target {
			return nil, errs.New(errs.CodeConflict, "不能禁用自己")
		}
		if err := s.guardLastAdmin(ctx, target); err != nil {
			return nil, err
		}
	}
	out, err := s.repo.SetEnabled(ctx, target, enabled)
	if err != nil {
		return nil, err
	}
	if !enabled {
		// best-effort：撤销会话失败不应让禁用操作回滚（enabled=false 已生效，
		// ResolveSession 会拒绝该用户）。
		_ = s.repo.DeleteSessionsByUserID(context.WithoutCancel(ctx), target)
	}
	s.record(ctx, &actor, "user.set_enabled", target.String())
	return out, nil
}

// AdminResetPassword 由管理员重置目标密码，生成强随机临时密码、撤销其全部
// 会话，并返回临时明文（仅此一次）。
func (s *service) AdminResetPassword(ctx context.Context, id uuid.UUID) (string, error) {
	if _, err := s.repo.GetUserByID(ctx, id); err != nil {
		return "", err
	}
	temp, err := randomPassword()
	if err != nil {
		return "", errs.Wrap(err, errs.CodeInternal, "generate temp password")
	}
	hash, err := HashPassword(temp)
	if err != nil {
		return "", errs.Wrap(err, errs.CodeInternal, "hash temp password")
	}
	if err := s.repo.UpdatePassword(ctx, id, hash); err != nil {
		return "", err
	}
	_ = s.repo.DeleteSessionsByUserID(context.WithoutCancel(ctx), id)
	s.record(ctx, &id, "user.reset_password", id.String())
	return temp, nil
}

// ChangePassword 用户自助改密：校验旧密码后更新，并撤销其全部会话（含当前），
// 强制用新密码重新登录。
func (s *service) ChangePassword(ctx context.Context, id uuid.UUID, oldPassword, newPassword string) error {
	u, err := s.repo.GetUserByID(ctx, id)
	if err != nil {
		return err
	}
	ok, err := VerifyPassword(oldPassword, u.PasswordHash)
	if err != nil {
		return errs.Wrap(err, errs.CodeInternal, "verify password")
	}
	if !ok {
		return errs.New(errs.CodeUnauthorized, "旧密码错误")
	}
	hash, err := HashPassword(newPassword)
	if err != nil {
		return errs.Wrap(err, errs.CodeInvalidInput, "password")
	}
	if err := s.repo.UpdatePassword(ctx, id, hash); err != nil {
		return err
	}
	_ = s.repo.DeleteSessionsByUserID(context.WithoutCancel(ctx), id)
	s.record(ctx, &id, "user.change_password", id.String())
	return nil
}

// DeleteUser 删除用户。同样施加自锁与最后管理员护栏。其会话经 DB 外键级联清除。
func (s *service) DeleteUser(ctx context.Context, actor, target uuid.UUID) error {
	if actor == target {
		return errs.New(errs.CodeConflict, "不能删除自己")
	}
	t, err := s.repo.GetUserByID(ctx, target)
	if err != nil {
		return err
	}
	if t.IsAdmin && t.Enabled {
		if err := s.guardLastAdmin(ctx, target); err != nil {
			return err
		}
	}
	if err := s.repo.DeleteUser(ctx, target); err != nil {
		return err
	}
	s.record(ctx, &actor, "user.delete", target.String())
	return nil
}

// guardLastAdmin 在降级/禁用/删除目标会导致“无启用管理员”时返回 CodeConflict。
// 仅当目标本身是启用管理员且系统启用管理员数 <= 1 时触发。
func (s *service) guardLastAdmin(ctx context.Context, target uuid.UUID) error {
	t, err := s.repo.GetUserByID(ctx, target)
	if err != nil {
		return err
	}
	if !t.IsAdmin || !t.Enabled {
		return nil
	}
	n, err := s.repo.CountAdmins(ctx)
	if err != nil {
		return err
	}
	if n <= 1 {
		return errs.New(errs.CodeConflict, "必须保留至少一个启用的管理员")
	}
	return nil
}

// record 是审计的薄封装：recorder 为 nil 时跳过；用 WithoutCancel 派生 ctx 保证
// 响应返回后审计仍能落库。
func (s *service) record(ctx context.Context, actor *uuid.UUID, action, targetID string) {
	if s.audit == nil {
		return
	}
	_ = s.audit.Record(context.WithoutCancel(ctx), audit.Entry{
		UserID:     actor,
		Action:     action,
		TargetType: "user",
		TargetID:   targetID,
	})
}

// randomPassword 生成一段强随机 base64 临时密码，用于管理员重置场景。
func randomPassword() (string, error) {
	tok, _, err := NewSessionToken()
	if err != nil {
		return "", err
	}
	if len(tok) > tempPasswordLen {
		tok = tok[:tempPasswordLen]
	}
	return tok, nil
}
