package auth

import (
	"crypto/rand"
	"errors"
	"fmt"
	"strings"

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

// CodeLength is the fixed length of the decimal code delivered by
// SendEmailCode. The OpenAPI description ("six-digit") locks the value
// at six; tests cannot lower it without breaking the contract test
// against the schema.
const CodeLength = 6

// codeBcryptCost is the bcrypt cost used to store the hashed code in
// auth_challenges.code_hash. Cost 10 matches the convention documented
// for admin password storage in `backend/README.md` §12. Six-digit codes
// have only ~1M entropy, so the bcrypt slowdown is what bounds online
// attacks together with the per-challenge attempt ceiling.
const codeBcryptCost = bcrypt.DefaultCost

// generateCode returns a random CodeLength-character decimal string. The
// modulo bias when mapping uniform bytes to ten digits is acceptable for
// short-lived registration codes — the per-challenge attempt ceiling and
// the TTL bound abuse far more tightly than the negligible bias.
func generateCode() (string, error) {
	digits := make([]byte, CodeLength)
	if _, err := rand.Read(digits); err != nil {
		return "", fmt.Errorf("auth: generate code: %w", err)
	}
	var sb strings.Builder
	sb.Grow(CodeLength)
	for _, b := range digits {
		sb.WriteByte('0' + b%10)
	}
	return sb.String(), nil
}

// hashCode returns the bcrypt hash of code using the package-level cost.
func hashCode(code string) ([]byte, error) {
	return bcrypt.GenerateFromPassword([]byte(code), codeBcryptCost)
}

// verifyCode reports whether code matches hash. The function is a thin
// wrapper around bcrypt.CompareHashAndPassword so the comparison is
// constant-time on the matching path. Returns nil on match,
// ErrCodeMismatch when the bcrypt mismatch error fires, and a wrapped
// error for any other failure (e.g. malformed hash).
func verifyCode(hash []byte, code string) error {
	err := bcrypt.CompareHashAndPassword(hash, []byte(code))
	if err == nil {
		return nil
	}
	if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
		return ErrCodeMismatch
	}
	return fmt.Errorf("auth: verify code: %w", err)
}
