package textutil

import "strings"

// Pluralize applies basic English pluralization rules to a label.
//
// Rules:
//
//   - empty input → empty (defensive guard for callers that pass
//     through unset Labels)
//
//   - ends in consonant + "y" → replace "y" with "ies"
//     (Opportunity → Opportunities)
//
//   - ends in "sh" / "ch" → append "es" (Dish → Dishes, Branch → Branches)
//
//   - ends in a sibilant "s" (ss / us / is) → append "es"
//     (Address → Addresses, Bus → Buses, Crisis → Crisises)
//
//     Note: this branch returns label + "es" (concatenation), NOT
//     substitution, so "Crisis" → "Crisises" rather than the
//     linguistically-canonical "Crises". This is what every existing
//     caller has relied on; pluralize_test.go pins it.
//
//   - ends in "x" → append "es" (Box → Boxes)
//
//   - ends in single "z" → append "zes" (Quiz → Quizzes)
//
//   - already-doubled "zz" → append "es" (Buzz → Buzzes)
//
//   - ends in non-sibilant "s" (consonant other than s/u/i preceding
//     the trailing "s") → label is treated as already-plural, returned
//     unchanged (Cars → Cars). See "SQL CASE divergence" below.
//
//   - default → append "s" (Account → Accounts)
//
// # Callers of record
//
//   - bootstrap/schema.go::initializeSchema (managed metadata batch
//     insert path; the bootstrap-time _System_Object writer)
//   - bootstrap/discover_tables.go (legacy physical-table discovery
//     and registration path)
//   - controlplane/packs/platform_core.go::buildObjectMetadata
//     (source-backed pack apply — entry point for every install-pack /
//     reapply via reconcileActiveInstalledPackSourceState; the reason
//     this function is exported)
//
// Before NX#492 the third caller defaulted to naive `Label + "s"`
// when the spec omitted PluralLabel, which produced "Opportunitys" /
// "Price Book Entrys" on every reapply of the source-backed CRM pack.
// Routing all three callers through Pluralize gives the same
// canonical plural at every write site for any Label whose canonical
// form diverges from naive `+s` (consonant-y, sibilants, x, z).
//
// # SQL twins (must change together)
//
// Two SQL expressions mirror this algorithm:
//
//   - bootstrap/schema.go::ensurePluralLabelsAndNameFieldsScoped —
//     every-bootstrap empty-fill backfill (UPDATE _System_Object
//     SET PluralLabel = CASE ... WHERE PluralLabel = '' OR IS NULL).
//   - bootstrap/migrate_legacy_naive_plurals.go::updateSQL — the
//     one-shot stamp-guarded data correction for legacy naive `+s`
//     rows.
//
// Any change to Pluralize's rule set MUST update both SQL twins in
// the same PR. The cross-implementation invariant is the contract
// REVIEWER_GUIDE cross-stage rule "SQL CASE 与 Go 函数的双实现：改一方
// 时同步检查另一方" anchors on; drifting one path produces silent bugs
// (no compile error, no test failure for inputs not covered by the
// pinned cases below).
//
// # Known divergences pinned by tests
//
//   - "Crisis" → "Crisises" (sibilant-is branch returns label+"es"
//     concatenation, not substitution to "Crises"). Matches SQL CASE.
//     Pinned by pluralize_test.go::sibilant_is_crisis_concatenation_not_substitution.
//   - "Cars" → "Cars" (non-sibilant trailing 's' returned unchanged
//     as already-plural). DIVERGES from SQL CASE, which would emit
//     "Carses" via the (s|x|z|sh|ch)$ branch. Pinned by
//     pluralize_test.go::non_sibilant_s_cars. No source-backed pack
//     currently has a Label of this shape; jq inventory of
//     official CRM pack payload verified at NX#492 PR #494
//     review round 4. Whoever introduces such a Label next is
//     responsible for converging the SQL twins per the rule above.
func Pluralize(label string) string {
	if label == "" {
		return ""
	}
	lower := strings.ToLower(label)
	n := len(lower)
	if n >= 2 {
		lastChar := lower[n-1]
		prevChar := lower[n-2]
		// consonant + y → ies
		if lastChar == 'y' && !strings.ContainsRune("aeiou", rune(prevChar)) {
			return label[:len(label)-1] + "ies"
		}
		// ends in "sh" or "ch" → es
		if (prevChar == 's' || prevChar == 'c') && lastChar == 'h' {
			return label + "es"
		}
		// sibilant "s" endings: ss, us, is → es
		if lastChar == 's' && (prevChar == 's' || prevChar == 'u' || prevChar == 'i') {
			return label + "es"
		}
	}
	// ends in x → es
	last := lower[n-1]
	if last == 'x' {
		return label + "es"
	}
	// ends in z → double the z + es (Quiz → Quizzes), but not if already
	// doubled (Buzz → Buzzes).
	if last == 'z' {
		if n >= 2 && lower[n-2] == 'z' {
			return label + "es"
		}
		return label + "zes"
	}
	// non-sibilant "s" → already plural (see Cars caveat in package
	// doc above).
	if last == 's' {
		return label
	}
	return label + "s"
}
