package identity

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

const (
	totpPeriod       = int64(30)
	totpDigits       = 6
	totpWindow       = int64(1)
	backupCodeCount  = 10
	backupCodeLength = 8
)

var backupAlphabet = []byte("23456789ABCDEFGHJKLMNPQRSTUVWXYZ")

type BackupCodeRecord struct {
	Hash   string     `json:"hash"`
	UsedAt *time.Time `json:"used_at,omitempty"`
}

func GenerateTOTPSecret() (string, error) {
	secret := make([]byte, 20)
	if _, err := rand.Read(secret); err != nil {
		return "", err
	}
	return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(secret), nil
}

func TOTPProvisioningURI(secret string, kind Kind, login string) string {
	issuer := "AI Gateway"
	label := issuer + ":" + string(kind) + ":" + login
	query := url.Values{
		"secret":    []string{secret},
		"issuer":    []string{issuer},
		"algorithm": []string{"SHA1"},
		"digits":    []string{fmt.Sprint(totpDigits)},
		"period":    []string{fmt.Sprint(totpPeriod)},
	}
	return "otpauth://totp/" + url.PathEscape(label) + "?" + query.Encode()
}

func VerifyTOTP(secret, code string, now time.Time) (int64, bool) {
	code = strings.TrimSpace(code)
	if len(code) != totpDigits {
		return 0, false
	}
	step := now.Unix() / totpPeriod
	for offset := -totpWindow; offset <= totpWindow; offset++ {
		candidate, err := hotp(secret, step+offset, totpDigits)
		if err == nil && subtle.ConstantTimeCompare([]byte(candidate), []byte(code)) == 1 {
			return step + offset, true
		}
	}
	return 0, false
}

func hotp(secret string, counter int64, digits int) (string, error) {
	if counter < 0 || digits < 6 || digits > 8 {
		return "", errors.New("invalid HOTP parameters")
	}
	decoded, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(strings.TrimSpace(secret)))
	if err != nil {
		return "", err
	}
	message := make([]byte, 8)
	binary.BigEndian.PutUint64(message, uint64(counter))
	mac := hmac.New(sha1.New, decoded)
	_, _ = mac.Write(message)
	digest := mac.Sum(nil)
	offset := digest[len(digest)-1] & 0x0f
	value := (uint32(digest[offset])&0x7f)<<24 |
		uint32(digest[offset+1])<<16 |
		uint32(digest[offset+2])<<8 |
		uint32(digest[offset+3])
	modulus := uint32(1)
	for i := 0; i < digits; i++ {
		modulus *= 10
	}
	return fmt.Sprintf("%0*d", digits, value%modulus), nil
}

func GenerateBackupCodes() ([]string, []BackupCodeRecord, error) {
	codes := make([]string, 0, backupCodeCount)
	records := make([]BackupCodeRecord, 0, backupCodeCount)
	for range backupCodeCount {
		random := make([]byte, backupCodeLength)
		if _, err := rand.Read(random); err != nil {
			return nil, nil, err
		}
		for index := range random {
			random[index] = backupAlphabet[int(random[index])%len(backupAlphabet)]
		}
		raw := string(random)
		code := raw[:4] + "-" + raw[4:]
		codes = append(codes, code)
		records = append(records, BackupCodeRecord{Hash: HashBackupCode(code)})
	}
	return codes, records, nil
}

func HashBackupCode(code string) string {
	normalized := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(code), "-", ""))
	digest := sha256.Sum256([]byte(normalized))
	return hex.EncodeToString(digest[:])
}
