package auth

import (
	"crypto/rand"
	"crypto/subtle"
	"encoding/hex"
	"errors"
	"fmt"
	"log"
	"strconv"
	"strings"
	"time"

	"golang.org/x/crypto/argon2"
	"golang.org/x/crypto/bcrypt"
)

var defaultPasswordManager = NewPassword()

// Password 密码管理器（Argon2id）
type Password struct {
	memory      uint32
	iterations  uint32
	parallelism uint8
	saltLength  uint32
	keyLength   uint32
}

// NewPassword 创建密码管理器
func NewPassword() *Password {
	return &Password{
		memory:      64 * 1024, // 64MB（符合 OWASP 建议）
		iterations:  5,         // 5 次迭代（保守值，高于 OWASP 建议的 3）
		parallelism: 4,         // 4 并行（符合 OWASP 建议，防御 GPU 破解）
		saltLength:  16,        // 16 字节盐（符合 OWASP 最低要求）
		keyLength:   32,        // 32 字节密钥
	}
}

// CalibrateArgon2id 在当前机器上自动校准 Argon2id 参数，确保单次哈希时间不超过 budget。
//
// 校准策略（优先保留 memory，其次降低 iterations）：
//  1. 用默认参数（64MB/5iter）测量一次哈希耗时。
//  2. 若耗时 ≤ budget，直接返回：默认参数已安全。
//  3. 若耗时 > budget，先尝试降低 iterations（最低 2）。
//  4. 若仍超预算，再二分降低 memory（最低 16MB）。
//  5. 若仍超预算，打印 warn 但不更改参数（避免参数过弱）。
//
// 建议在 main() 启动阶段调用一次，结果会更新全局 defaultPasswordManager。
// budget 推荐值：500ms（登录接口 P99 目标 < 1000ms，留出网络/DB 余量）。
func CalibrateArgon2id(budget time.Duration) {
	if budget <= 0 {
		budget = 500 * time.Millisecond
	}

	probe := func(mem uint32, iter uint32, par uint8) time.Duration {
		salt := make([]byte, 16)
		_, _ = rand.Read(salt)
		start := time.Now()
		_ = argon2.IDKey([]byte("calibration-probe"), salt, iter, mem, par, 32)
		return time.Since(start)
	}

	mem := defaultPasswordManager.memory
	iter := defaultPasswordManager.iterations
	par := defaultPasswordManager.parallelism

	elapsed := probe(mem, iter, par)
	log.Printf("argon2id calibration: default params (m=%dKB, t=%d, p=%d) → %v", mem, iter, par, elapsed)

	if elapsed <= budget {
		log.Printf("argon2id calibration: default params are within budget (%v ≤ %v), no adjustment needed", elapsed, budget)
		return
	}

	// Step 1：尝试降低 iterations（最低 2，低于 2 不满足 OWASP 最低要求）
	for iter > 2 {
		iter--
		elapsed = probe(mem, iter, par)
		log.Printf("argon2id calibration: trying m=%dKB t=%d p=%d → %v", mem, iter, par, elapsed)
		if elapsed <= budget {
			break
		}
	}

	// Step 2：若仍超预算，二分降低 memory（最低 16MB = 16*1024 KiB）
	if elapsed > budget {
		const minMem = 16 * 1024
		for mem > minMem && elapsed > budget {
			mem /= 2
			if mem < minMem {
				mem = minMem
			}
			elapsed = probe(mem, iter, par)
			log.Printf("argon2id calibration: trying m=%dKB t=%d p=%d → %v", mem, iter, par, elapsed)
		}
	}

	if elapsed > budget {
		log.Printf("argon2id calibration: WARN — even minimum params (m=%dKB, t=%d) take %v > %v; check server load", mem, iter, elapsed, budget)
		// 不降低到不安全参数，保持当前已尝试的最低值
	} else {
		log.Printf("argon2id calibration: adjusted params m=%dKB t=%d p=%d → %v (budget: %v)", mem, iter, par, elapsed, budget)
	}

	// 更新全局默认管理器（仅在此阶段修改，后续不再变更）
	defaultPasswordManager.memory = mem
	defaultPasswordManager.iterations = iter
}


// Hash 哈希密码（使用Argon2id + 随机盐）
func (p *Password) Hash(password string) (string, error) {
	// 使用 crypto/rand 生成真正随机的盐
	salt := make([]byte, p.saltLength)
	if _, err := rand.Read(salt); err != nil {
		return "", fmt.Errorf("生成随机盐失败: %w", err)
	}

	// 使用Argon2id哈希密码
	hash := argon2.IDKey(
		[]byte(password),
		salt,
		p.iterations,
		p.memory,
		p.parallelism,
		p.keyLength,
	)

	// 格式: $argon2id$v=<version>$m=<memory>,t=<iterations>,p=<parallelism>$<salt_hex>$<hash_hex>
	encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
		argon2.Version,
		p.memory,
		p.iterations,
		p.parallelism,
		hex.EncodeToString(salt),
		hex.EncodeToString(hash),
	)

	return encoded, nil
}

// Verify 验证密码
func (p *Password) Verify(hashedPassword, password string) bool {
	// 支持 bcrypt 格式（兼容旧数据）
	if strings.HasPrefix(hashedPassword, "$2a$") || strings.HasPrefix(hashedPassword, "$2b$") {
		err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
		return err == nil
	}

	// 解析 Argon2id 格式
	parts := strings.Split(hashedPassword, "$")
	// 格式: ["", "argon2id", "v=<version>", "m=<mem>,t=<iter>,p=<par>", "<salt_hex>", "<hash_hex>"]
	if len(parts) != 6 || parts[1] != "argon2id" {
		return false
	}

	// 解析参数
	var memory, iterations uint32
	var parallelism uint8
	params := strings.Split(parts[3], ",")
	if len(params) != 3 {
		return false
	}
	for _, param := range params {
		kv := strings.SplitN(param, "=", 2)
		if len(kv) != 2 {
			return false
		}
		val, err := strconv.ParseUint(kv[1], 10, 64)
		if err != nil {
			return false
		}
		switch kv[0] {
		case "m":
			// #nosec G115 - argon2 memory param is constrained by spec to reasonable values
			memory = uint32(val) // #nosec G115
		case "t":
			// #nosec G115 - argon2 iterations param is constrained by spec to reasonable values
			iterations = uint32(val) // #nosec G115
		case "p":
			// #nosec G115 - argon2 parallelism param is constrained by spec to reasonable values
			parallelism = uint8(val) // #nosec G115
		}
	}

	// 解码盐和存储的哈希
	salt, err := hex.DecodeString(parts[4])
	if err != nil {
		return false
	}
	storedHash, err := hex.DecodeString(parts[5])
	if err != nil {
		return false
	}

	// 用相同参数重新计算哈希
	// #nosec G115 - bcrypt hash is typically 60 chars, fits in uint32
	computedHash := argon2.IDKey(
		[]byte(password),
		salt,
		iterations,
		memory,
		parallelism,
		uint32(len(storedHash)), // #nosec G115
	)

	// 常数时间比较，防止时序攻击
	return subtle.ConstantTimeCompare(storedHash, computedHash) == 1
}

// HashPassword hashes passwords with Argon2id for new credentials.
func HashPassword(password string) (string, error) {
	return defaultPasswordManager.Hash(password)
}

// VerifyPassword verifies both Argon2id and legacy bcrypt password hashes.
func VerifyPassword(hashedPassword, password string) bool {
	return defaultPasswordManager.Verify(hashedPassword, password)
}

// ErrInvalidPassword 密码无效错误
var ErrInvalidPassword = errors.New("密码无效")

// BcryptHash 使用bcrypt哈希密码（兼容性支持）
func BcryptHash(password string) (string, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", fmt.Errorf("bcrypt加密失败: %w", err)
	}
	return string(hash), nil
}

// BcryptVerify 使用bcrypt验证密码
func BcryptVerify(hashedPassword, password string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
	return err == nil
}
