package packs

import (
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strings"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// child_relationship_validation.go houses the substrate-level validators
// for issue #153 Track B. The declared-name uniqueness gate is wired into
// source-backed validate-pack and pack construction because it only applies
// when an L1 pack has explicitly authored childRelationshipName.
//
// The stricter F7 presence gate remains deliberately unwired until the L1 pack
// payload populates childRelationshipName on every reference / master-detail
// FK. That keeps `main` consistent with today's partially migrated payload
// while still rejecting conflicting declared names.

// ChildRelFKDecl is the input shape for both validators. It mirrors the
// minimum fields a pack manifest declares about a reference / master-detail
// FK; the follow-up cutover PR's validate-pack adapter will convert FieldSpec
// rows into this shape.
type ChildRelFKDecl struct {
	// ObjectAPIName is the API name of the object on which the FK field lives
	// (the child). Example: "Contact" for Contact.AccountId.
	ObjectAPIName string
	// FieldAPIName is the API name of the FK field itself.
	// Example: "AccountId".
	FieldAPIName string
	// ParentAPIName is the parent the FK points at. For polymorphic FKs the
	// caller must expand to one ChildRelFKDecl per parent.
	ParentAPIName string
	// ChildRelationshipName is the value the pack proposes to write into
	// _System_Field.ChildRelationshipName. Empty / whitespace-only strings
	// are rejected by ValidateChildRelationshipNamesPresent (F7).
	ChildRelationshipName string
}

func childRelFKDeclsFromObjectSpecs(specs []ObjectSpec) []ChildRelFKDecl {
	return ChildRelFKDeclsFromObjectSpecs(specs)
}

func ChildRelFKDeclsFromObjectSpecs(specs []ObjectSpec) []ChildRelFKDecl {
	var decls []ChildRelFKDecl
	for _, spec := range specs {
		objectAPIName := strings.TrimSpace(spec.APIName)
		if objectAPIName == "" {
			continue
		}
		for _, field := range spec.Fields {
			sem := childRelationshipFieldSemantics(field)
			if !sem.IsRelationship {
				continue
			}
			fieldAPIName := strings.TrimSpace(field.APIName)
			for _, parent := range sem.ReferenceTargets {
				parent = strings.TrimSpace(parent)
				if parent == "" {
					continue
				}
				decls = append(decls, ChildRelFKDecl{
					ObjectAPIName:         objectAPIName,
					FieldAPIName:          fieldAPIName,
					ParentAPIName:         parent,
					ChildRelationshipName: field.ChildRelationshipName,
				})
			}
		}
	}
	return decls
}

func ChildRelFKDeclsFromObjectSpecPayloads(payloads [][]byte, label string) ([]ChildRelFKDecl, error) {
	specs, err := ParseObjectSpecsPayloads(payloads, label)
	if err != nil {
		return nil, err
	}
	return ChildRelFKDeclsFromObjectSpecs(specs), nil
}

func childRelationshipFieldSemantics(field FieldSpec) models.FieldRelationshipSemantics {
	return models.ResolveFieldRelationshipSemantics(&models.FieldMetadata{
		Type:                constants.SchemaFieldType(field.Type),
		ReferenceTo:         field.ReferenceTo,
		WritableReferenceTo: field.WritableReferenceTo,
		IsPolymorphic:       field.IsPolymorphic,
	}, models.LifecycleAuthoredWrite)
}

// trimmed returns a copy with whitespace stripped from every string field so
// downstream code can compare without re-trimming on every access.
func (d ChildRelFKDecl) trimmed() ChildRelFKDecl {
	return ChildRelFKDecl{
		ObjectAPIName:         strings.TrimSpace(d.ObjectAPIName),
		FieldAPIName:          strings.TrimSpace(d.FieldAPIName),
		ParentAPIName:         strings.TrimSpace(d.ParentAPIName),
		ChildRelationshipName: strings.TrimSpace(d.ChildRelationshipName),
	}
}

// ErrChildRelNameMissing is returned by ValidateChildRelationshipNamesPresent
// for any FK declaration whose ChildRelationshipName is empty or
// whitespace-only. The cutover PR maps this to a fail-closed validate-pack
// diagnostic per F7.
var ErrChildRelNameMissing = errors.New("packs: reference / master-detail FK is missing childRelationshipName")

// ErrChildRelNameDuplicate is returned by ValidateChildRelationshipUniqueness
// when two FK declarations propose the same childRelationshipName for the
// same parent (case-insensitive). The cutover PR maps this to fail-closed
// validate-pack and verify-pack diagnostics per Refinement 3.
var ErrChildRelNameDuplicate = errors.New("packs: duplicate childRelationshipName within parent's child-namespace")

// ErrChildRelNameInvalid is returned when an authored childRelationshipName
// cannot safely become the SOQL child-subquery identifier read by runtime.
var ErrChildRelNameInvalid = errors.New("packs: invalid childRelationshipName")

var childRelationshipNameIdentifier = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`)

// childRelationshipSOQLReservedTokens is a fail-closed install-boundary guard
// for names that would be parsed as SOQL SELECT syntax instead of a single
// child-subquery relationship identifier. Keep this local to packs: importing
// the CRM SOQL dialect lexer here would pull an L1 dialect dependency back into
// the app-blind backend module.
var childRelationshipSOQLReservedTokens = lowerSet([]string{
	// Core SELECT/subquery clauses.
	"ALL",
	"AND",
	"ASC",
	"BY",
	"DESC",
	"FOR",
	"FROM",
	"GROUP",
	"HAVING",
	"LIMIT",
	"OFFSET",
	"ORDER",
	"SELECT",
	"UPDATE",
	"USING",
	"WHERE",
	"WITH",

	// TYPEOF and contextual SOQL clauses.
	"ELSE",
	"END",
	"REFERENCE",
	"SCOPE",
	"THEN",
	"TYPEOF",
	"VIEW",
	"WHEN",

	// ORDER BY null placement.
	"FIRST",
	"LAST",
	"NULLS",

	// Operators and literals.
	"EXCLUDES",
	"FALSE",
	"IN",
	"INCLUDES",
	"LIKE",
	"NOT",
	"NULL",
	"OR",
	"TRUE",

	// FIELDS(...) selectors.
	"CUSTOM",
	"DISTINCT",
	"FIELDS",
	"STANDARD",

	// WITH DATA CATEGORY selectors.
	"ABOVE",
	"ABOVE_OR_BELOW",
	"AT",
	"BELOW",
	"CATEGORY",
	"DATA",

	// WITH mode/security modifiers.
	"SECURITY_ENFORCED",
	"SYSTEM_MODE",
	"USER_MODE",

	// Aggregate, grouping, date, currency, and geolocation functions that occupy
	// identifier-shaped token positions in SELECT / ORDER BY grammar.
	"AVG",
	"CALENDAR_MONTH",
	"CALENDAR_QUARTER",
	"CALENDAR_YEAR",
	"CONVERTCURRENCY",
	"COUNT",
	"COUNT_DISTINCT",
	"CUBE",
	"DAY_IN_MONTH",
	"DAY_IN_WEEK",
	"DAY_IN_YEAR",
	"DAY_ONLY",
	"DISTANCE",
	"FISCAL_MONTH",
	"FISCAL_QUARTER",
	"FISCAL_YEAR",
	"FORMAT",
	"GEOLOCATION",
	"GROUPING",
	"HOUR_IN_DAY",
	"MAX",
	"MIN",
	"ROLLUP",
	"SUM",
	"TOLABEL",
	"TOSTRING",
	"WEEK_IN_MONTH",
	"WEEK_IN_YEAR",
})

// ValidateChildRelationshipNamesPresent (F7) reports every FK declaration
// whose ChildRelationshipName is empty / whitespace-only. The error wraps
// ErrChildRelNameMissing and lists the offending declarations in stable
// (object, field) order so caller diagnostics are reproducible.
//
// Returns nil when every declaration carries a populated name.
func ValidateChildRelationshipNamesPresent(decls []ChildRelFKDecl) error {
	type bad struct{ obj, field string }
	var missing []bad
	for _, d := range decls {
		t := d.trimmed()
		if t.ChildRelationshipName == "" {
			missing = append(missing, bad{obj: t.ObjectAPIName, field: t.FieldAPIName})
		}
	}
	if len(missing) == 0 {
		return nil
	}
	sort.Slice(missing, func(i, j int) bool {
		if missing[i].obj != missing[j].obj {
			return missing[i].obj < missing[j].obj
		}
		return missing[i].field < missing[j].field
	})
	parts := make([]string, 0, len(missing))
	for _, b := range missing {
		parts = append(parts, fmt.Sprintf("%s.%s", b.obj, b.field))
	}
	return fmt.Errorf("%w: %s", ErrChildRelNameMissing, strings.Join(parts, ", "))
}

// ValidateChildRelationshipNameSyntax reports authored non-empty
// childRelationshipName values that cannot safely serve as a single SOQL
// child-subquery identifier. Blank names are deliberately skipped here; the
// cutover presence gate owns them.
func ValidateChildRelationshipNameSyntax(decls []ChildRelFKDecl) error {
	type invalid struct {
		decl   ChildRelFKDecl
		reason string
	}
	var invalids []invalid
	for _, d := range decls {
		rawName := d.ChildRelationshipName
		t := d.trimmed()
		name := t.ChildRelationshipName
		if name == "" {
			continue
		}
		var reason string
		switch {
		case rawName != name:
			reason = "must not contain leading or trailing whitespace"
		case !childRelationshipNameIdentifier.MatchString(name):
			reason = "must start with an ASCII letter and contain only ASCII letters, digits, or underscores"
		case strings.HasSuffix(name, "_"):
			reason = "must not end with underscore"
		case childRelationshipSOQLReservedTokens[strings.ToLower(name)]:
			reason = "must not be a SOQL reserved word"
		}
		if reason != "" {
			display := t
			display.ChildRelationshipName = rawName
			invalids = append(invalids, invalid{decl: display, reason: reason})
		}
	}
	if len(invalids) == 0 {
		return nil
	}
	sort.Slice(invalids, func(i, j int) bool {
		li := childRelationshipDeclSortKey(invalids[i].decl)
		lj := childRelationshipDeclSortKey(invalids[j].decl)
		if li != lj {
			return li < lj
		}
		return invalids[i].reason < invalids[j].reason
	})
	parts := make([]string, 0, len(invalids))
	for _, item := range invalids {
		parts = append(parts, fmt.Sprintf("%s.%s -> %s childRelationshipName %q %s",
			item.decl.ObjectAPIName,
			item.decl.FieldAPIName,
			item.decl.ParentAPIName,
			item.decl.ChildRelationshipName,
			item.reason,
		))
	}
	return fmt.Errorf("%w: %s", ErrChildRelNameInvalid, strings.Join(parts, "; "))
}

// ValidateChildRelationshipNameParentFieldCollisions rejects declared child
// relationship names that collide with a field API name on the same parent
// object inside the source payload. Runtime SOQL child subqueries and field
// paths must not compete for the same parent-object identifier.
func ValidateChildRelationshipNameParentFieldCollisions(decls []ChildRelFKDecl, specs []ObjectSpec) error {
	fieldsByObject := objectSpecFieldNameIndex(specs)
	return checkChildRelationshipParentFieldCollisions(decls, fieldsByObject)
}

func objectSpecFieldNameIndex(specs []ObjectSpec) map[string]map[string]string {
	fieldsByObject := make(map[string]map[string]string)
	for _, spec := range specs {
		objectAPIName := strings.TrimSpace(spec.APIName)
		if objectAPIName == "" {
			continue
		}
		objectKey := strings.ToLower(objectAPIName)
		if fieldsByObject[objectKey] == nil {
			fieldsByObject[objectKey] = make(map[string]string)
		}
		for _, field := range spec.Fields {
			fieldAPIName := strings.TrimSpace(field.APIName)
			if fieldAPIName == "" {
				continue
			}
			fieldsByObject[objectKey][strings.ToLower(fieldAPIName)] = fieldAPIName
		}
	}
	return fieldsByObject
}

func checkChildRelationshipParentFieldCollisions(decls []ChildRelFKDecl, fieldsByObject map[string]map[string]string) error {
	type collision struct {
		decl      ChildRelFKDecl
		fieldName string
	}
	reservedFieldNames := l0ReservedFieldNameSet()
	var collisions []collision
	for _, d := range decls {
		t := d.trimmed()
		if t.ChildRelationshipName == "" || t.ParentAPIName == "" {
			continue
		}
		nameKey := strings.ToLower(t.ChildRelationshipName)
		if reservedFieldNames[nameKey] {
			collisions = append(collisions, collision{decl: t, fieldName: t.ChildRelationshipName})
			continue
		}
		parentFields := fieldsByObject[strings.ToLower(t.ParentAPIName)]
		if fieldName := parentFields[nameKey]; fieldName != "" {
			collisions = append(collisions, collision{decl: t, fieldName: fieldName})
		}
	}
	if len(collisions) == 0 {
		return nil
	}
	sort.Slice(collisions, func(i, j int) bool {
		li := childRelationshipDeclSortKey(collisions[i].decl)
		lj := childRelationshipDeclSortKey(collisions[j].decl)
		if li != lj {
			return li < lj
		}
		return collisions[i].fieldName < collisions[j].fieldName
	})
	parts := make([]string, 0, len(collisions))
	for _, c := range collisions {
		parts = append(parts, fmt.Sprintf("%s.%s -> %s childRelationshipName %q collides with parent field %s.%s",
			c.decl.ObjectAPIName,
			c.decl.FieldAPIName,
			c.decl.ParentAPIName,
			c.decl.ChildRelationshipName,
			c.decl.ParentAPIName,
			c.fieldName,
		))
	}
	return fmt.Errorf("%w: %s", ErrChildRelNameInvalid, strings.Join(parts, "; "))
}

func ValidateChildRelationshipDeclarations(specs []ObjectSpec) error {
	decls := ChildRelFKDeclsFromObjectSpecs(specs)
	return errors.Join(
		ValidateChildRelationshipNameSyntax(decls),
		ValidateChildRelationshipNameParentFieldCollisions(decls, specs),
		ValidateChildRelationshipUniqueness(decls),
	)
}

// ValidateChildRelationshipUniqueness (Refinement 3) reports per-parent
// duplicate childRelationshipName declarations. Comparison is
// case-insensitive on (parent, name). Empty names are skipped here — they
// belong to ValidateChildRelationshipNamesPresent.
//
// Returns nil when every (parent, name) pair is unique.
func ValidateChildRelationshipUniqueness(decls []ChildRelFKDecl) error {
	type seenEntry struct{ object, field string }
	seen := make(map[string][]seenEntry)
	for _, d := range decls {
		t := d.trimmed()
		if t.ChildRelationshipName == "" || t.ParentAPIName == "" {
			continue
		}
		key := strings.ToLower(t.ParentAPIName) + "|" + strings.ToLower(t.ChildRelationshipName)
		seen[key] = append(seen[key], seenEntry{object: t.ObjectAPIName, field: t.FieldAPIName})
	}
	type collisionReport struct {
		key     string
		members []seenEntry
	}
	var collisions []collisionReport
	for k, members := range seen {
		if len(members) > 1 {
			collisions = append(collisions, collisionReport{key: k, members: members})
		}
	}
	if len(collisions) == 0 {
		return nil
	}
	sort.Slice(collisions, func(i, j int) bool {
		return collisions[i].key < collisions[j].key
	})
	parts := make([]string, 0, len(collisions))
	for _, c := range collisions {
		members := append([]seenEntry(nil), c.members...)
		sort.Slice(members, func(i, j int) bool {
			if members[i].object != members[j].object {
				return members[i].object < members[j].object
			}
			return members[i].field < members[j].field
		})
		memberStrs := make([]string, 0, len(members))
		for _, m := range members {
			memberStrs = append(memberStrs, fmt.Sprintf("%s.%s", m.object, m.field))
		}
		parts = append(parts, fmt.Sprintf("(%s) -> [%s]", c.key, strings.Join(memberStrs, ", ")))
	}
	return fmt.Errorf("%w: %s", ErrChildRelNameDuplicate, strings.Join(parts, "; "))
}

func childRelationshipDeclSortKey(d ChildRelFKDecl) string {
	return strings.ToLower(strings.TrimSpace(d.ParentAPIName)) + "|" +
		strings.ToLower(strings.TrimSpace(d.ChildRelationshipName)) + "|" +
		strings.ToLower(strings.TrimSpace(d.ObjectAPIName)) + "." +
		strings.ToLower(strings.TrimSpace(d.FieldAPIName))
}

// LiveChildRelFKRow describes a single row currently active (or installing)
// in _System_Field, as needed by CheckInstallPackChildRelationshipCollisions
// (F2 + F6). The cutover PR's install-pack adapter projects from
// _System_Field + _System_InstalledPack into this shape.
//
// SourcePack is the OwnerPack / source-pack stamp on the row. It is used by
// the F6 self-rewrite exemption: a proposed write whose
// (SourcePack, ObjectAPIName, FieldAPIName) matches an existing live row is
// idempotent re-apply, not a collision.
type LiveChildRelFKRow struct {
	SourcePack            string
	ObjectAPIName         string
	FieldAPIName          string
	ParentAPIName         string
	ChildRelationshipName string
}

type LiveParentFieldRow struct {
	ObjectAPIName string
	FieldAPIName  string
}

func (r LiveChildRelFKRow) trimmed() LiveChildRelFKRow {
	return LiveChildRelFKRow{
		SourcePack:            strings.TrimSpace(r.SourcePack),
		ObjectAPIName:         strings.TrimSpace(r.ObjectAPIName),
		FieldAPIName:          strings.TrimSpace(r.FieldAPIName),
		ParentAPIName:         strings.TrimSpace(r.ParentAPIName),
		ChildRelationshipName: strings.TrimSpace(r.ChildRelationshipName),
	}
}

// CheckInstallPackChildRelationshipCollisions (F2 + F6) checks whether any
// of the proposed FK writes collide with the live `(parent, name)`
// namespace. Self-rewrite is exempt: a proposed write whose
// (proposedPack, ObjectAPIName, FieldAPIName) tuple matches an existing live
// row's (SourcePack, ObjectAPIName, FieldAPIName) is treated as idempotent
// re-apply.
//
// Both proposed and live decls are normalized case-insensitively on
// (parent, name). The proposed pack stamp comes from `proposedPack` because
// the proposed declarations have not yet been written and therefore have no
// SourcePack of their own at decision time.
//
// Per F2's lane-gap analysis, callers are expected to feed `live` from the
// union of (Status='active', Status='installing') _System_Field rows so two
// concurrent installs cannot bypass each other's `installing` rows.
//
// Returns nil when every proposal either matches no live row OR matches only
// its own self-rewrite. Returns an error wrapping ErrChildRelNameDuplicate
// for any cross-pack / cross-field collision.
func CheckInstallPackChildRelationshipCollisions(
	proposedPack string,
	proposed []ChildRelFKDecl,
	live []LiveChildRelFKRow,
) error {
	pack := strings.TrimSpace(proposedPack)

	type liveEntry struct{ sourcePack, object, field string }
	byParentName := make(map[string][]liveEntry)
	for _, r := range live {
		t := r.trimmed()
		if t.ParentAPIName == "" || t.ChildRelationshipName == "" {
			continue
		}
		key := strings.ToLower(t.ParentAPIName) + "|" + strings.ToLower(t.ChildRelationshipName)
		byParentName[key] = append(byParentName[key], liveEntry{
			sourcePack: t.SourcePack,
			object:     t.ObjectAPIName,
			field:      t.FieldAPIName,
		})
	}

	type collisionReport struct{ proposal, conflict string }
	var collisions []collisionReport
	for _, p := range proposed {
		t := p.trimmed()
		if t.ChildRelationshipName == "" || t.ParentAPIName == "" {
			continue
		}
		key := strings.ToLower(t.ParentAPIName) + "|" + strings.ToLower(t.ChildRelationshipName)
		for _, lr := range byParentName[key] {
			if isSelfRewrite(pack, t, lr.sourcePack, lr.object, lr.field) {
				continue
			}
			collisions = append(collisions, collisionReport{
				proposal: fmt.Sprintf("(%s.%s -> %s as %q)",
					t.ObjectAPIName, t.FieldAPIName, t.ParentAPIName, t.ChildRelationshipName),
				conflict: fmt.Sprintf("(%s.%s owned by pack=%q)", lr.object, lr.field, lr.sourcePack),
			})
		}
	}
	if len(collisions) == 0 {
		return nil
	}
	sort.Slice(collisions, func(i, j int) bool {
		if collisions[i].proposal != collisions[j].proposal {
			return collisions[i].proposal < collisions[j].proposal
		}
		return collisions[i].conflict < collisions[j].conflict
	})
	parts := make([]string, 0, len(collisions))
	for _, c := range collisions {
		parts = append(parts, fmt.Sprintf("%s collides with %s", c.proposal, c.conflict))
	}
	return fmt.Errorf("%w: %s", ErrChildRelNameDuplicate, strings.Join(parts, "; "))
}

func CheckInstallPackChildRelationshipParentFieldCollisions(proposed []ChildRelFKDecl, live []LiveParentFieldRow) error {
	fieldsByObject := make(map[string]map[string]string)
	for _, row := range live {
		objectAPIName := strings.TrimSpace(row.ObjectAPIName)
		fieldAPIName := strings.TrimSpace(row.FieldAPIName)
		if objectAPIName == "" || fieldAPIName == "" {
			continue
		}
		objectKey := strings.ToLower(objectAPIName)
		if fieldsByObject[objectKey] == nil {
			fieldsByObject[objectKey] = make(map[string]string)
		}
		fieldsByObject[objectKey][strings.ToLower(fieldAPIName)] = fieldAPIName
	}
	return checkChildRelationshipParentFieldCollisions(proposed, fieldsByObject)
}

// isSelfRewrite implements F6: anchor on (source_pack, object, field_api_name),
// not on PK or write timestamp. A pack uninstall+reinstall may regenerate row
// IDs but the (pack, object, field) triple is stable, so it is the right
// identity for "this is the same FK rewriting itself". Inputs are expected to
// be pre-trimmed by the caller.
func isSelfRewrite(proposedPack string, proposed ChildRelFKDecl, liveSourcePack, liveObject, liveField string) bool {
	if proposedPack == "" || liveSourcePack == "" {
		return false
	}
	return strings.EqualFold(proposedPack, liveSourcePack) &&
		strings.EqualFold(proposed.ObjectAPIName, liveObject) &&
		strings.EqualFold(proposed.FieldAPIName, liveField)
}
