package main

import (
	"sort"
	"strings"

	"github.com/lukaszraczylo/go-telegram/internal/spec"
)

// enumDecl is one generated enum: a Go type alias of string plus a set
// of named constants. Values keep doc order; constant identifiers are
// derived from values via constName.
type enumDecl struct {
	Name   string
	Values []string
}

// enumPlan is the deduplicated, name-resolved set of enums emitted from
// an API IR. Lookup returns the enum name for a given field reference;
// All returns the deterministic-ordered list of declarations to emit.
type enumPlan struct {
	// fieldKey -> enum name. The fieldKey is a string built by enumKey.
	byField map[string]string
	// enum name -> declaration.
	decls map[string]enumDecl
}

// enumKey identifies a single Field occurrence so the emitter can look
// up the enum name later. Parent is the type name for struct fields and
// methodEnumParent(name) for method params, so two methods sharing a
// param name never alias each other's enum (or non-enum) fields.
func enumKey(parent, fieldName string) string { return parent + "::" + fieldName }

// methodEnumParent is the enum-plan key parent for a method's params.
// The "method:" prefix keeps it disjoint from type names.
func methodEnumParent(methodName string) string { return "method:" + methodName }

// planEnums walks the IR, decides on enum names, deduplicates, and
// returns an enumPlan. All scraper-marked enum fields are covered.
func planEnums(api *spec.API) *enumPlan {
	type ref struct {
		parent    string // naming parent ("" for method params)
		keyParent string // byField key parent (methodEnumParent(...) for method params)
		fieldName string
		jsonName  string
		values    []string
		valueKey  string // canonical key for value-set dedup
	}

	// Unification pass: for each sealed-interface union, fold per-variant
	// single-value enum fields that share a discriminator name into ONE
	// unified enum at union level. Claimed (parent,fieldName) tuples are
	// excluded from the per-field grouping below.
	unifiedDecls, unifiedByField := planUnifiedUnionEnums(api)
	claimed := func(parent, fieldName string) bool {
		_, ok := unifiedByField[enumKey(parent, fieldName)]
		return ok
	}

	var refs []ref
	collect := func(parent, keyParent string, fields []spec.Field) {
		for _, f := range fields {
			if len(f.EnumValues) == 0 {
				continue
			}
			if claimed(parent, f.Name) {
				continue
			}
			refs = append(refs, ref{
				parent:    parent,
				keyParent: keyParent,
				fieldName: f.Name,
				jsonName:  f.JSONName,
				values:    f.EnumValues,
				valueKey:  valueKey(f.EnumValues),
			})
		}
	}
	for _, t := range api.Types {
		collect(t.Name, t.Name, t.Fields)
	}
	for _, m := range api.Methods {
		// Method params have no shared Go parent type, so the naming
		// parent is "" (the default-name heuristic still produces the
		// right answer for ParseMode-style enums), but the byField key
		// is method-scoped so a same-named non-enum param on another
		// method can never pick up this enum.
		collect("", methodEnumParent(m.Name), m.Params)
	}

	// candidate name per ref (before collision resolution)
	candidate := make([]string, len(refs))
	for i, r := range refs {
		candidate[i] = defaultEnumName(r.parent, r.jsonName, r.fieldName)
	}

	// Group by valueKey to coalesce identical value-sets across fields.
	// Choose canonical name: prefer the most common candidate; tie-break
	// by shortest name; final tie-break alphabetical.
	type groupInfo struct {
		values []string
		name   string
		first  int
	}
	groups := map[string]*groupInfo{}
	for i, r := range refs {
		g, ok := groups[r.valueKey]
		if !ok {
			groups[r.valueKey] = &groupInfo{values: r.values, first: i}
			g = groups[r.valueKey]
		}
		_ = g
	}
	// Rank candidate names per group.
	for vk := range groups {
		counts := map[string]int{}
		hasParent := map[string]bool{}
		var names []string
		for i, r := range refs {
			if r.valueKey != vk {
				continue
			}
			n := candidate[i]
			if _, ok := counts[n]; !ok {
				names = append(names, n)
			}
			counts[n]++
			if r.parent != "" {
				hasParent[n] = true
			}
		}
		// Pick the canonical name for this group:
		//   1. highest occurrence count wins;
		//   2. names that originated from a parent type win over plain
		//      method-param candidates (avoids "Format"-style
		//      monosyllables);
		//   3. shortest name wins;
		//   4. alphabetical for full determinism.
		sort.SliceStable(names, func(a, b int) bool {
			if counts[names[a]] != counts[names[b]] {
				return counts[names[a]] > counts[names[b]]
			}
			if hasParent[names[a]] != hasParent[names[b]] {
				return hasParent[names[a]]
			}
			if len(names[a]) != len(names[b]) {
				return len(names[a]) < len(names[b])
			}
			return names[a] < names[b]
		})
		groups[vk].name = names[0]
	}

	// Collision pass: two groups must not share the same enum name.
	// When that happens, suffix the loser(s) with their parent type
	// name so the result is unique. Iterate in deterministic order
	// (groups sorted by valueKey).
	used := map[string]string{} // name -> valueKey owner
	var keys []string
	for vk := range groups {
		keys = append(keys, vk)
	}
	sort.Strings(keys)
	for _, vk := range keys {
		g := groups[vk]
		if _, taken := used[g.name]; !taken {
			used[g.name] = vk
			continue
		}
		// Find a unique name by prepending a parent prefix from one of
		// the contributing refs (the lowest-index ref in this group).
		for i, r := range refs {
			if r.valueKey != vk {
				continue
			}
			if r.parent == "" {
				continue
			}
			cand := r.parent + goNamePart(r.jsonName)
			if _, taken := used[cand]; !taken {
				g.name = cand
				used[cand] = vk
				goto next
			}
			_ = i
		}
		// Fallback: append a numeric disambiguator. Should not happen
		// in practice for the Telegram docs but keeps the algorithm
		// total.
		for n := 2; ; n++ {
			cand := groups[vk].name + itoa(n)
			if _, taken := used[cand]; !taken {
				g.name = cand
				used[cand] = vk
				break
			}
		}
	next:
	}

	// Build the plan.
	plan := &enumPlan{
		byField: map[string]string{},
		decls:   map[string]enumDecl{},
	}
	for i, r := range refs {
		name := groups[r.valueKey].name
		plan.byField[enumKey(r.keyParent, r.fieldName)] = name
		_ = i
	}
	for vk, g := range groups {
		plan.decls[g.name] = enumDecl{Name: g.name, Values: g.values}
		_ = vk
	}
	// Merge unified union enums (already named with stutter handling and
	// keyed per-variant in unifiedByField).
	for k, name := range unifiedByField {
		plan.byField[k] = name
	}
	for name, d := range unifiedDecls {
		plan.decls[name] = d
	}
	return plan
}

// planUnifiedUnionEnums detects sealed-interface unions whose variants
// share a single discriminator field with one enum value each, and emits
// ONE unified enum per union covering all variant values. Returns the
// declarations to emit and the per-(variant,fieldName) map to point each
// variant's field at the unified enum.
//
// A union qualifies when EVERY variant in t.OneOf:
//  1. defines a field with the same Go-name (e.g. "Status", "Type", "Source");
//  2. that field is a required string with len(EnumValues)==1.
//
// The picked Go-name is the first one tried in this priority order:
//   - knownDiscriminators[union].Field's Go-name (resolved via JSONName match);
//   - "Type", "Status", "Source" (the three discriminators Telegram uses).
//
// First match wins; if none qualify, the union is skipped (variants keep
// their existing per-field treatment, which still single-emits via the
// regular grouping pass).
func planUnifiedUnionEnums(api *spec.API) (map[string]enumDecl, map[string]string) {
	decls := map[string]enumDecl{}
	byField := map[string]string{}

	typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
	for i := range api.Types {
		typeByName[api.Types[i].Name] = &api.Types[i]
	}

	// Iterate unions in deterministic (declaration) order.
	for ui := range api.Types {
		u := &api.Types[ui]
		if len(u.OneOf) == 0 {
			continue
		}

		// Resolve the variants. Skip unions where any variant is missing
		// (defensive — shouldn't happen in a well-formed IR).
		variants := make([]*spec.TypeDecl, 0, len(u.OneOf))
		for _, vName := range u.OneOf {
			v, ok := typeByName[vName]
			if !ok {
				variants = nil
				break
			}
			variants = append(variants, v)
		}
		if len(variants) == 0 {
			continue
		}

		// Build the candidate Go-name list. Priority order:
		//  1. discriminator GoField from knownDiscriminators (resolved via JSONName);
		//  2. "Type", "Status", "Source".
		var candidateNames []string
		seen := map[string]bool{}
		add := func(name string) {
			if name == "" || seen[name] {
				return
			}
			seen[name] = true
			candidateNames = append(candidateNames, name)
		}
		if ds, ok := knownDiscriminators[u.Name]; ok && ds.Field != "" {
			// Resolve Go-name from the first variant whose field matches the JSON name.
			for _, v := range variants {
				for _, f := range v.Fields {
					if f.JSONName == ds.Field {
						add(f.Name)
						break
					}
				}
			}
		}
		for _, n := range []string{"Type", "Status", "Source"} {
			add(n)
		}

		// Find the first candidate Go-name where every variant has a
		// matching single-value string-enum field.
		var (
			pickedName string
			pickedDocs map[string]spec.Field // variant name -> field
		)
		for _, name := range candidateNames {
			matches := map[string]spec.Field{}
			ok := true
			for _, v := range variants {
				var hit *spec.Field
				for fi := range v.Fields {
					if v.Fields[fi].Name == name {
						hit = &v.Fields[fi]
						break
					}
				}
				if hit == nil ||
					hit.Type.Kind != spec.KindPrimitive ||
					hit.Type.Name != "string" ||
					len(hit.EnumValues) != 1 {
					ok = false
					break
				}
				matches[v.Name] = *hit
			}
			if ok {
				pickedName = name
				pickedDocs = matches
				break
			}
		}
		if pickedName == "" {
			continue
		}

		// Build the unified enum name with stutter handling.
		enumName := unifiedEnumName(u.Name, pickedName)

		// Collect values across variants in deterministic order, deduping.
		valueOrder := make([]string, 0, len(variants))
		valueSeen := map[string]bool{}
		for _, v := range u.OneOf {
			f := pickedDocs[v]
			val := f.EnumValues[0]
			if valueSeen[val] {
				continue
			}
			valueSeen[val] = true
			valueOrder = append(valueOrder, val)
		}

		decls[enumName] = enumDecl{Name: enumName, Values: valueOrder}
		for _, v := range variants {
			byField[enumKey(v.Name, pickedName)] = enumName
		}
	}

	return decls, byField
}

// unifiedEnumName builds the union-level enum name. Falls back to a
// "Kind" suffix when the naive concatenation reads as a stutter:
//
//   - union name ends in the field name verbatim (e.g. BackgroundType+Type);
//   - union name ends in any "concept noun" — Type/Status/Source/State —
//     so appending another such noun would duplicate the suffix
//     (e.g. ChatBoostSource+Source, RevenueWithdrawalState+Type).
//
// Otherwise the natural concatenation wins (ChatMember+Status →
// ChatMemberStatus, MessageOrigin+Type → MessageOriginType).
func unifiedEnumName(unionName, fieldName string) string {
	for _, suf := range []string{"Type", "Status", "Source", "State"} {
		if strings.HasSuffix(unionName, suf) {
			return unionName + "Kind"
		}
	}
	return unionName + fieldName
}

// All returns the enum declarations sorted by name for deterministic emit.
func (p *enumPlan) All() []enumDecl {
	out := make([]enumDecl, 0, len(p.decls))
	for _, d := range p.decls {
		out = append(out, d)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
	return out
}

// FieldEnum returns the enum name for a field on a given parent type
// (use parent="" for method parameters), or "" if the field is not an
// enum.
func (p *enumPlan) FieldEnum(parent, fieldName string) string {
	if p == nil {
		return ""
	}
	return p.byField[enumKey(parent, fieldName)]
}

// ConstFor returns the collision-resolved constant identifier for value
// within the named enum declaration. Falls back to the plain constName
// when the declaration is unknown (unit tests with partial plans).
func (p *enumPlan) ConstFor(enumName, value string) string {
	if p != nil {
		if d, ok := p.decls[enumName]; ok {
			return d.ConstName(value)
		}
	}
	return constName(enumName, value)
}

// defaultEnumName picks an initial Go enum name for a field. parse_mode
// fields collapse to the canonical "ParseMode"; otherwise the name is
// parent + PascalCase(jsonName).
func defaultEnumName(parent, jsonName, fieldName string) string {
	if strings.HasSuffix(jsonName, "parse_mode") {
		return "ParseMode"
	}
	return parent + goNamePart(jsonName)
}

// constName builds a Go constant identifier "<EnumName><PascalValue>"
// from a wire value. Slashes (mime types) become "Of" so
// "image/jpeg" → "ImageOfJpeg".
func constName(enumName, value string) string {
	return enumName + valuePascal(value)
}

// ConstName returns the constant identifier for value within this enum,
// resolving case-collisions between values that Pascal-case to the same
// identifier (e.g. RichBlockListItem.type values "a" and "A"). Colliding
// values get a Lower/Upper prefix on the value part based on the case of
// the value's first letter; any residual collision gets a numeric suffix.
func (e enumDecl) ConstName(value string) string {
	counts := map[string]int{}
	for _, v := range e.Values {
		counts[constName(e.Name, v)]++
	}
	plain := constName(e.Name, value)
	if counts[plain] <= 1 {
		return plain
	}
	cased := e.Name + casePrefix(value) + valuePascal(value)
	// Residual collision (same value text repeated, or Lower/Upper still
	// ambiguous): append the value's 1-based position for determinism.
	casedCounts := map[string]int{}
	pos := 0
	for i, v := range e.Values {
		if counts[constName(e.Name, v)] > 1 {
			casedCounts[e.Name+casePrefix(v)+valuePascal(v)]++
		}
		if v == value {
			pos = i + 1
		}
	}
	if casedCounts[cased] > 1 {
		return cased + itoa(pos)
	}
	return cased
}

// casePrefix distinguishes case-colliding enum values: "Lower" when the
// value starts with a lowercase letter, "Upper" otherwise.
func casePrefix(v string) string {
	if v != "" && v[0] >= 'a' && v[0] <= 'z' {
		return "Lower"
	}
	return "Upper"
}

func valuePascal(v string) string {
	// "image/jpeg" → "ImageOfJpeg"
	parts := strings.Split(v, "/")
	for i, p := range parts {
		parts[i] = goNamePart(p)
	}
	return strings.Join(parts, "Of")
}

// goNamePart converts a snake_case (or already-PascalCase) token to
// PascalCase, mirroring scrape.goName behaviour without the acronym
// special-cases (which apply to wire identifiers, not enum values).
func goNamePart(s string) string {
	if s == "" {
		return ""
	}
	parts := strings.Split(s, "_")
	var b strings.Builder
	for _, p := range parts {
		if p == "" {
			continue
		}
		// Acronyms used in Telegram wire names. Keeping in sync with
		// scrape/table.go avoids divergent capitalisation between
		// fieldName and constName.
		switch p {
		case "id":
			b.WriteString("ID")
			continue
		case "url":
			b.WriteString("URL")
			continue
		case "ip":
			b.WriteString("IP")
			continue
		case "https":
			b.WriteString("HTTPS")
			continue
		case "json":
			b.WriteString("JSON")
			continue
		case "html":
			b.WriteString("HTML")
			continue
		}
		if c := p[0]; c >= 'a' && c <= 'z' {
			b.WriteByte(c - 'a' + 'A')
			b.WriteString(p[1:])
		} else {
			b.WriteString(p)
		}
	}
	return b.String()
}

func valueKey(values []string) string {
	cp := make([]string, len(values))
	copy(cp, values)
	sort.Strings(cp)
	return strings.Join(cp, "\x00")
}

func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	var buf [20]byte
	i := len(buf)
	for n > 0 {
		i--
		buf[i] = byte('0' + n%10)
		n /= 10
	}
	return string(buf[i:])
}
