// Package idgen generates application-level identifiers.
// For most entities it wraps github.com/google/uuid (v7 time-ordered UUIDs).
// For activation codes it produces 16-character Crockford Base32 strings
// with a check digit, suitable for display and human entry.
package idgen

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"

	"github.com/google/uuid"
)

// ─────────────────────────────────────────────────────────────────────────────
// UUID v7 — time-ordered identifiers for database entities
// ─────────────────────────────────────────────────────────────────────────────

// New returns a new UUID v7 (time-ordered, random node bits).
// UUID v7 is the recommended identifier format for database entities; its
// time-ordered structure improves B-tree index locality compared with UUID v4.
// Panics only if the system's crypto/rand source is unavailable (a fatal
// misconfiguration; panic is appropriate).
func New() uuid.UUID {
	id, err := uuid.NewV7()
	if err != nil {
		panic("idgen: crypto/rand unavailable: " + err.Error())
	}
	return id
}

// NewString returns a new UUID v7 as a canonical lower-case string
// (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
func NewString() string {
	return New().String()
}

// ─────────────────────────────────────────────────────────────────────────────
// Crockford Base32 — human-friendly activation codes
// ─────────────────────────────────────────────────────────────────────────────

// crockfordAlphabet is the 32-symbol encoding alphabet (excludes I, L, O, U
// to prevent visual confusion with 1, 1, 0, and V respectively).
const crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"

// crockfordCheck is the extended 37-symbol check-character alphabet used for
// the Crockford mod-37 check symbol. Symbols 0–31 match crockfordAlphabet;
// symbols 32–36 are *, ~, $, =, U.
const crockfordCheck = "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"

// crockfordDecode maps every printable ASCII character to its Crockford
// numeric value (0–31), or to -1 if the character is not valid.
// Normalisation rules per the Crockford spec are applied at init time:
//
//	I, i, l, L → 1
//	O, o       → 0
var crockfordDecode [128]int8

func init() {
	for i := range crockfordDecode {
		crockfordDecode[i] = -1
	}
	for i, ch := range crockfordAlphabet {
		crockfordDecode[ch] = int8(i)
		// Accept lower-case equivalents.
		if ch >= 'A' && ch <= 'Z' {
			crockfordDecode[ch-'A'+'a'] = int8(i)
		}
	}
	// Normalisation: I/i/l/L → 1; O/o → 0.
	crockfordDecode['I'] = crockfordDecode['1']
	crockfordDecode['i'] = crockfordDecode['1']
	crockfordDecode['l'] = crockfordDecode['1']
	crockfordDecode['L'] = crockfordDecode['1']
	crockfordDecode['O'] = crockfordDecode['0']
	crockfordDecode['o'] = crockfordDecode['0']
}

// computeCheckValue computes the Crockford mod-37 check value (Horner's method)
// of the first 15 characters of s (which must already be in canonical form).
// Returns -1 if any character is invalid.
func computeCheckValue(s string) int {
	result := 0
	for i := 0; i < 15; i++ {
		ch := s[i]
		if ch >= 128 {
			return -1
		}
		v := int(crockfordDecode[ch])
		if v < 0 {
			return -1
		}
		result = (result*32 + v) % 37
	}
	return result
}

// validateCheckChar verifies that canonical[15] is the correct Crockford
// mod-37 check symbol for canonical[0:15].
func validateCheckChar(canonical string) error {
	if len(canonical) != 16 {
		return errors.New("idgen: invalid length for check validation")
	}
	expected := computeCheckValue(canonical)
	if expected < 0 {
		return errors.New("idgen: invalid data characters in code")
	}
	want := rune(crockfordCheck[expected])
	got := rune(canonical[15])
	if got != want {
		return fmt.Errorf("idgen: check character mismatch: want %c, got %c", want, got)
	}
	return nil
}

// CanonicalizeCode converts an activation-code string into its canonical form:
// uppercase, with I/L→1 and O→0 substitutions applied, with the check
// character validated. Hyphens and spaces are stripped before validation
// (supports the common XXXX-XXXX-XXXX-XXXX display format).
//
// Returns an error if the string contains characters outside the normalised
// Crockford alphabet, if the length (after stripping) is not exactly 16, or
// if the check character is incorrect.
func CanonicalizeCode(code string) (string, error) {
	code = strings.TrimSpace(code)
	code = strings.ReplaceAll(code, "-", "")
	code = strings.ReplaceAll(code, " ", "")

	if len(code) != 16 {
		return "", fmt.Errorf("idgen: code must be exactly 16 characters, got %d", len(code))
	}

	var buf [16]byte
	for i := 0; i < 16; i++ {
		ch := code[i]
		if ch >= 128 {
			return "", fmt.Errorf("idgen: non-ASCII character at position %d", i)
		}
		v := crockfordDecode[ch]
		if v < 0 {
			if i < 15 {
				return "", fmt.Errorf("idgen: invalid character %q at position %d", ch, i)
			}
			// Position 15 is the check character; validate separately below.
			buf[i] = []byte(strings.ToUpper(string(ch)))[0]
			continue
		}
		buf[i] = crockfordAlphabet[v]
	}

	canonical := string(buf[:])
	if err := validateCheckChar(canonical); err != nil {
		return "", err
	}
	return canonical, nil
}

// HashCode returns the hex-encoded SHA-256 digest of the canonical plaintext
// activation code. This digest is the value stored in the database; the
// plaintext code itself must never be persisted or logged.
func HashCode(canonical string) string {
	sum := sha256.Sum256([]byte(canonical))
	return hex.EncodeToString(sum[:])
}

// GenerateCode generates a single random activation code in canonical
// Crockford Base32 form: 15 random data characters followed by 1 mod-37
// check character (16 characters total). Uses crypto/rand for
// cryptographically-secure randomness.
//
// To avoid modular bias, the function uses rejection sampling: random bytes
// in [0, 224) are accepted (224 = 7×32 ensures a uniform distribution over
// the 32-symbol alphabet), bytes ≥ 224 are discarded.
func GenerateCode() (string, error) {
	const dataLen = 15
	var buf [dataLen]byte
	i := 0
	for i < dataLen {
		// Over-read to reduce crypto/rand syscall count.
		var tmp [dataLen * 2]byte
		if _, err := rand.Read(tmp[:]); err != nil {
			return "", fmt.Errorf("idgen: crypto/rand: %w", err)
		}
		for _, b := range tmp {
			if b < 224 { // accept range: 224 = 7×32
				buf[i] = crockfordAlphabet[b%32]
				i++
				if i == dataLen {
					break
				}
			}
		}
	}

	data := string(buf[:])
	// Compute check value over the 15 data chars (the dummy 16th char is ignored).
	checkVal := computeCheckValue(data + "0")
	if checkVal < 0 {
		// Cannot happen: all buf[i] values come from crockfordAlphabet.
		return "", errors.New("idgen: internal check computation error")
	}
	return data + string(crockfordCheck[checkVal]), nil
}
