// Package user 内的 auth.go 提供与认证相关的纯函数工具。
//
// 现已覆盖两组：
//   - 密码：HashPassword 用 argon2id 默认参数（i=1, m=64MiB, p=4, saltLen=16,
//     keyLen=32）签发哈希；VerifyPassword 做常量时间比对。
//   - 会话 token：NewSessionToken 生成 24 字节随机原文（base64 RawURL 编码后
//     32 字符）并同时返回 sha256 摘要；HashSessionToken 是纯函数，便于服务端
//     根据请求里携带的明文 token 反查 DB 中存储的哈希。
//
// 设计取舍：
//   - 业务规则在工具层而非 Service 层校验密码长度（minPasswordLen=8），
//     以确保任何调用 HashPassword 的入口（包括 Bootstrap、改密、迁移脚本）
//     都享受相同的下限，无需重复实现策略。
//   - VerifyPassword 对 argon2id.ErrInvalidHash 显式返回 (false, nil)：
//     存量数据中如果出现非 argon2id 格式（例如历史明文或其它算法的残留），
//     调用方应当作 “认证失败” 处理而非系统错误，避免登录入口因为脏数据 500。
//   - Session token 明文只在登录响应里下发一次，DB 仅保存 sha256 摘要：
//     即便 DB 泄漏，攻击者也无法直接拿哈希去发起认证（sha256 不可逆，且服务端
//     入口走 “明文 → 哈希反查” 路径）；同时 sha256 是固定 32 字节，便于建唯一
//     索引、避免变长字段比对的歧义。
package user

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"

	"github.com/alexedwards/argon2id"
)

// sessionTokenRawLen 是会话 token 的随机字节长度。
// 24 字节 = 192 bit 熵；base64 RawURL 编码后正好 32 字符且无 padding，
// 既保证强度又避免 URL 转义问题。
const sessionTokenRawLen = 24

// minPasswordLen 是创建账号或修改密码时允许的最短长度。
// 8 字符是最低基线，更复杂的强度策略（字典、复用检查）留待业务侧扩展。
const minPasswordLen = 8

// HashPassword 用 argon2id 默认参数对明文密码进行哈希。
// 当明文长度不足 minPasswordLen 时返回错误，不会调用底层算法。
func HashPassword(plain string) (string, error) {
	if len(plain) < minPasswordLen {
		return "", fmt.Errorf("password must be at least %d chars", minPasswordLen)
	}
	hash, err := argon2id.CreateHash(plain, argon2id.DefaultParams)
	if err != nil {
		return "", fmt.Errorf("hash: %w", err)
	}
	return hash, nil
}

// VerifyPassword 将明文密码与已存哈希做常量时间比对。
// 哈希格式不合法时返回 (false, nil)，让调用方按 “认证失败” 处理；其它
// 底层错误（例如参数解析失败）原样上抛。
func VerifyPassword(plain, hash string) (bool, error) {
	ok, err := argon2id.ComparePasswordAndHash(plain, hash)
	if err != nil && !errors.Is(err, argon2id.ErrInvalidHash) {
		return false, err
	}
	return ok, nil
}

// NewSessionToken 返回 (明文 token, sha256(token), err)。
// 明文只在登录响应里返回一次；DB 只存 hash，并通过 “明文 → sha256 反查”
// 校验请求合法性。
func NewSessionToken() (token string, hash []byte, err error) {
	raw := make([]byte, sessionTokenRawLen)
	if _, err := rand.Read(raw); err != nil {
		return "", nil, fmt.Errorf("rand: %w", err)
	}
	tok := base64.RawURLEncoding.EncodeToString(raw)
	return tok, HashSessionToken(tok), nil
}

// HashSessionToken 对会话 token 明文做 sha256 摘要，返回 32 字节固定长度。
// 纯函数：相同输入恒返回相同输出，便于服务端按请求中的明文反查 DB。
func HashSessionToken(token string) []byte {
	sum := sha256.Sum256([]byte(token))
	return sum[:]
}
