// Package ident — единственная точка генерации и разбора идентификаторов
// домена. Идентификатор — ULID (26 символов Crockford base32), канонический
// вид — lowercase; на входных границах (URL, формы) значение прогоняется
// через Parse до обращения к хранилищу, потому что сравнение строк в SQLite
// побайтовое.
package ident

import (
	"crypto/rand"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/oklog/ulid/v2"
)

// entropy — monotonic-источник: при равной миллисекунде timestamp'а энтропия
// инкрементируется, так что id, выданные подряд, сохраняют порядок выдачи.
var (
	mu      sync.Mutex
	entropy = ulid.Monotonic(rand.Reader, 0)
)

// NewID возвращает новый идентификатор (текущее время).
func NewID() string {
	return NewIDAt(time.Now())
}

// NewIDAt возвращает идентификатор с timestamp-частью из t. Используется
// миграциями для бэкфилла: сортировка id сохраняет историческую хронологию
// (created_at имеет секундное разрешение — равные метки упорядочивает
// monotonic-энтропия в порядке вызовов).
func NewIDAt(t time.Time) string {
	mu.Lock()
	defer mu.Unlock()
	id := ulid.MustNew(ulid.Timestamp(t.UTC()), entropy)
	return strings.ToLower(id.String())
}

// Parse валидирует внешний идентификатор и нормализует его к каноническому
// lowercase-виду. Регистр входа не важен (base32 ULID case-insensitive).
func Parse(s string) (string, error) {
	s = strings.TrimSpace(s)
	u, err := ulid.ParseStrict(strings.ToUpper(s))
	if err != nil {
		return "", fmt.Errorf("ident: parse %q: %w", s, err)
	}
	return strings.ToLower(u.String()), nil
}
