package verificationimp

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

	"github.com/tech/sendico/pkg/model"
)

type TokenHasher interface {
	Hash(raw string, token *model.VerificationToken) string
}

type magicHasher struct{}

func (magicHasher) Hash(raw string, _ *model.VerificationToken) string {
	return tokenHash(raw)
}

type otpHasher struct{}

func (otpHasher) Hash(raw string, t *model.VerificationToken) string {
	return otpHash(raw, *t.Salt)
}

func hasherFor(t *model.VerificationToken) TokenHasher {
	if t.Salt != nil {
		return otpHasher{}
	}
	return magicHasher{}
}

const verificationTokenBytes = 32
const otpDigits = 6

func generateMagic() (raw, hash string, err error) {
	rawBytes := make([]byte, verificationTokenBytes)
	if _, err = rand.Read(rawBytes); err != nil {
		return
	}
	raw = base64.RawURLEncoding.EncodeToString(rawBytes)
	hash = tokenHash(raw)
	return
}

func generateDeterministicMagic(seed string) (raw, hash string) {
	sum := sha256.Sum256([]byte("magic:" + seed))
	raw = base64.RawURLEncoding.EncodeToString(sum[:])
	hash = tokenHash(raw)
	return
}

func generateOTP() (code, salt, hash string, err error) {
	// otpDigits-digit code
	n := make([]byte, 4)
	if _, err = rand.Read(n); err != nil {
		return
	}
	num := int(n[0])<<24 | int(n[1])<<16 | int(n[2])<<8 | int(n[3])

	mod := 1
	for i := 0; i < otpDigits; i++ {
		mod *= 10
	}

	code = fmt.Sprintf("%0*d", otpDigits, num%mod)

	// per-token salt
	saltBytes := make([]byte, 16)
	if _, err = rand.Read(saltBytes); err != nil {
		return
	}
	salt = base64.RawURLEncoding.EncodeToString(saltBytes)

	hash = otpHash(code, salt)
	return
}

func generateDeterministicOTP(seed string) (code, salt, hash string) {
	sum := sha256.Sum256([]byte("otp:" + seed))

	num := int(sum[0])<<24 | int(sum[1])<<16 | int(sum[2])<<8 | int(sum[3])
	mod := 1
	for i := 0; i < otpDigits; i++ {
		mod *= 10
	}
	code = fmt.Sprintf("%0*d", otpDigits, num%mod)

	salt = base64.RawURLEncoding.EncodeToString(sum[4:20])
	hash = otpHash(code, salt)
	return
}

// We store only the resulting hash (+salt) in DB, never the OTP itself.
func otpHash(code, salt string) string {
	sum := sha256.Sum256([]byte(salt + ":" + code))
	return hex.EncodeToString(sum[:])
}

func tokenHash(rawToken string) string {
	hash := sha256.Sum256([]byte(rawToken))
	return base64.RawURLEncoding.EncodeToString(hash[:])
}
