// Package totp implements RFC 6238 time-based one-time passwords (TOTP) on
// top of RFC 4226 HOTP, using HMAC-SHA1, 6 digits, and a 30-second step.
//
// It is deliberately dependency-free (standard library only) so it can be
// shared between the admin backend two-factor login and the user-center 2FA
// (doc/05) without pulling in a third-party OTP package.
package totp

import (
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha1"
	"crypto/subtle"
	"encoding/base32"
	"encoding/binary"
	"fmt"
	"net/url"
	"strings"
	"time"
)

const (
	// Digits is the number of decimal digits in a generated code.
	Digits = 6
	// Period is the time step length.
	Period = 30 * time.Second
	// secretBytes is the length of a freshly generated shared secret. 20 bytes
	// (160 bits) matches the RFC 4226 recommendation and the SHA-1 block size.
	secretBytes = 20
)

// b32 is the no-padding, upper-case Base32 encoding used for OTP secrets
// (the alphabet authenticator apps expect).
var b32 = base32.StdEncoding.WithPadding(base32.NoPadding)

// GenerateSecret returns a new cryptographically random Base32-encoded secret.
func GenerateSecret() (string, error) {
	buf := make([]byte, secretBytes)
	if _, err := rand.Read(buf); err != nil {
		return "", fmt.Errorf("totp.GenerateSecret: %w", err)
	}
	return b32.EncodeToString(buf), nil
}

// Code returns the TOTP code for the given Base32 secret at time t.
func Code(secret string, t time.Time) (string, error) {
	key, err := decodeSecret(secret)
	if err != nil {
		return "", err
	}
	counter := uint64(t.UTC().Unix()) / uint64(Period.Seconds())
	return hotp(key, counter), nil
}

// Validate reports whether code is a valid TOTP for secret at time t, allowing
// ±skew steps of clock drift (skew=1 accepts the previous, current, and next
// 30-second windows). Comparison is constant-time.
func Validate(secret, code string, t time.Time, skew int) bool {
	key, err := decodeSecret(secret)
	if err != nil {
		return false
	}
	code = strings.TrimSpace(code)
	if len(code) != Digits {
		return false
	}
	if skew < 0 {
		skew = 0
	}
	base := int64(uint64(t.UTC().Unix()) / uint64(Period.Seconds()))
	for d := -skew; d <= skew; d++ {
		c := base + int64(d)
		if c < 0 {
			continue
		}
		want := hotp(key, uint64(c))
		if subtle.ConstantTimeCompare([]byte(want), []byte(code)) == 1 {
			return true
		}
	}
	return false
}

// ProvisioningURI builds an otpauth:// URI suitable for rendering as a QR code
// or pasting into an authenticator app.
func ProvisioningURI(secret, account, issuer string) string {
	label := url.PathEscape(issuer + ":" + account)
	q := url.Values{}
	q.Set("secret", secret)
	q.Set("issuer", issuer)
	q.Set("algorithm", "SHA1")
	q.Set("digits", fmt.Sprintf("%d", Digits))
	q.Set("period", fmt.Sprintf("%d", int(Period.Seconds())))
	return "otpauth://totp/" + label + "?" + q.Encode()
}

// decodeSecret accepts a Base32 secret with or without padding/whitespace.
func decodeSecret(secret string) ([]byte, error) {
	s := strings.ToUpper(strings.TrimSpace(secret))
	s = strings.ReplaceAll(s, " ", "")
	s = strings.TrimRight(s, "=")
	key, err := b32.DecodeString(s)
	if err != nil {
		return nil, fmt.Errorf("totp: invalid secret: %w", err)
	}
	if len(key) == 0 {
		return nil, fmt.Errorf("totp: empty secret")
	}
	return key, nil
}

// hotp implements RFC 4226 HOTP with dynamic truncation.
func hotp(key []byte, counter uint64) string {
	var buf [8]byte
	binary.BigEndian.PutUint64(buf[:], counter)

	mac := hmac.New(sha1.New, key)
	mac.Write(buf[:])
	sum := mac.Sum(nil)

	offset := sum[len(sum)-1] & 0x0f
	value := (uint32(sum[offset]&0x7f) << 24) |
		(uint32(sum[offset+1]) << 16) |
		(uint32(sum[offset+2]) << 8) |
		uint32(sum[offset+3])

	mod := uint32(1)
	for i := 0; i < Digits; i++ {
		mod *= 10
	}
	return fmt.Sprintf("%0*d", Digits, value%mod)
}
