package auth

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"strconv"
	"strings"
	"unicode"
)

// CheckPassword reports whether a password satisfies the configured policy,
// returning an error phrased for the end user (the caller can surface it
// verbatim on a signup or change-password form).
func (a *Authenticator) CheckPassword(password string) error {
	return CheckPasswordPolicy(a.cfg.Password, password)
}

// CheckPasswordPolicy is CheckPassword against an explicit policy, for callers
// that validate a password without an Authenticator in hand.
func CheckPasswordPolicy(policy PasswordPolicy, password string) error {
	if strings.TrimSpace(password) == "" {
		return errors.New("Password cannot be blank.")
	}

	if len(password) < policy.MinLength {
		return errors.New("Password must be at least " + strconv.Itoa(policy.MinLength) + " characters long.")
	}

	var uppercase, lowercase, numbers, symbols int
	for _, r := range password {
		switch {
		case unicode.IsUpper(r):
			uppercase++
		case unicode.IsLower(r):
			lowercase++
		case unicode.IsNumber(r):
			numbers++
		default:
			symbols++
		}
	}

	switch {
	case uppercase < policy.RequiredUppercase:
		return errors.New("Password must contain at least " + strconv.Itoa(policy.RequiredUppercase) + " uppercase character(s).")
	case lowercase < policy.RequiredLowercase:
		return errors.New("Password must contain at least " + strconv.Itoa(policy.RequiredLowercase) + " lowercase character(s).")
	case numbers < policy.RequiredNumbers:
		return errors.New("Password must contain at least " + strconv.Itoa(policy.RequiredNumbers) + " number(s).")
	case symbols < policy.RequiredSymbols:
		return errors.New("Password must contain at least " + strconv.Itoa(policy.RequiredSymbols) + " symbol(s).")
	}

	return nil
}

// GeneratePasswordResetToken returns a 32-byte crypto-random token, hex-encoded
// (64 characters). The app stores it against the user with an expiry and mails
// the same string out.
func GeneratePasswordResetToken() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}
