// Package pinyinsearch produces materialized pinyin representations of names
// for substring search. It returns both the full pinyin ("zhangsan") and the
// first-letter initials ("zs") so callers can match either typing style.
package pinyinsearch

import (
	"strings"
	"unicode"

	"github.com/mozillazg/go-pinyin"
)

// MaxFullLength matches the varchar(255) of display_name_pinyin / name_pinyin.
// MaxInitialsLength matches the varchar(64) of *_pinyin_initials columns.
const (
	MaxFullLength     = 255
	MaxInitialsLength = 64
)

// Variants returns the lowercased pinyin representations of name useful for
// substring search:
//
//   - full: each Han rune replaced by its normal-tone pinyin
//     ("张三" -> "zhangsan"); ASCII alphanumerics are lowercased and passed
//     through; other runes (emoji, punctuation, non-Han CJK) are dropped.
//   - initials: each Han rune maps to its first letter ("张三" -> "zs"); each
//     maximal ASCII alphanumeric run contributes its first letter
//     ("alice" -> "a"). This keeps the field compact (acronym-style) so it
//     fits the varchar(64) column even for long English names.
//
// Both outputs are capped to their storage column sizes; pathological inputs
// (extremely long names) lose suffix characters rather than failing the write.
//
// Non-empty inputs always produce non-empty outputs: when the input has no Han
// runes and no ASCII alphanumerics (pure punctuation, emoji, whitespace, CJK
// kana, etc.) each return value is a single period ".". This guarantees the
// caller's "unprocessed" sentinel (empty string) is unambiguous — the backfill
// relies on this to terminate. "." is chosen because TiDB's utf8mb4_bin is a
// PAD SPACE collation, so a trailing-space sentinel would still compare equal
// to the empty string; "." is non-space, non-alphanumeric (so Variants never
// produces it for a name with real pinyin content), and not a LIKE wildcard.
//
// Multi-rune Chinese names are joined with no separator, matching how users
// type search queries. Polyphone disambiguation uses go-pinyin's default
// frequency-based heuristic; rare misreadings are accepted as a tradeoff.
func Variants(name string) (full, initials string) {
	if name == "" {
		return "", ""
	}
	var fullParts, initialParts []string
	var asciiRun []byte
	flushASCIIRun := func() {
		if len(asciiRun) == 0 {
			return
		}
		initialParts = append(initialParts, strings.ToLower(string(asciiRun[0])))
		fullParts = append(fullParts, strings.ToLower(string(asciiRun)))
		asciiRun = asciiRun[:0]
	}
	for _, r := range name {
		switch {
		case r < 128:
			if isAlnum(byte(r)) {
				asciiRun = append(asciiRun, byte(r))
			} else {
				flushASCIIRun()
			}
		case unicode.Is(unicode.Han, r):
			flushASCIIRun()
			full, initial := hanPinyin(r)
			if full != "" {
				fullParts = append(fullParts, full)
			}
			if initial != "" {
				initialParts = append(initialParts, initial)
			}
		default:
			flushASCIIRun()
		}
	}
	flushASCIIRun()
	full = clampRunes(strings.Join(fullParts, ""), MaxFullLength)
	initials = clampRunes(strings.Join(initialParts, ""), MaxInitialsLength)
	if full == "" {
		full = "."
	}
	if initials == "" {
		initials = "."
	}
	return full, initials
}

func clampRunes(s string, max int) string {
	if max <= 0 {
		return ""
	}
	runes := []rune(s)
	if len(runes) <= max {
		return s
	}
	return string(runes[:max])
}

func hanPinyin(r rune) (full, initial string) {
	normal := pinyin.Pinyin(string(r), pinyin.Args{Style: pinyin.Normal})
	if len(normal) == 0 || len(normal[0]) == 0 {
		return "", ""
	}
	// Take the first reading for deterministic materialization. Polyphone
	// handling is intentionally simple — the common-case single-reading
	// coverage is what matters for search UX.
	fullReading := normal[0][0]

	firstLetter := pinyin.Pinyin(string(r), pinyin.Args{Style: pinyin.FirstLetter})
	if len(firstLetter) > 0 && len(firstLetter[0]) > 0 {
		return fullReading, firstLetter[0][0]
	}
	return fullReading, ""
}

func isAlnum(b byte) bool {
	switch {
	case b >= 'a' && b <= 'z':
		return true
	case b >= 'A' && b <= 'Z':
		return true
	case b >= '0' && b <= '9':
		return true
	}
	return false
}

// NormalizeQuery prepares a user-typed search query for substring comparison
// against materialized pinyin columns. ASCII alphanumerics are lowercased and
// kept; anything else (whitespace, punctuation, CJK) is dropped so that
// queries like "zs" or "zhangsan" match cleanly.
func NormalizeQuery(query string) string {
	var b strings.Builder
	for _, r := range query {
		switch {
		case r < 128:
			if isAlnum(byte(r)) {
				b.WriteByte(byte(unicode.ToLower(r)))
			}
		}
	}
	return b.String()
}
