package catalogsnapshot

import (
	"fmt"
	historysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	"sort"
	"strings"

	"github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	layoutauthoringv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
	layoutauthoringv2 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv2"
)

// ValidateLayoutAuthoringReferences resolves every schema-owned reference in
// one authored Layout against this exact immutable Catalog snapshot. Renderer,
// action-kind, component, and extension vocabularies are intentionally absent:
// they are app-owned opaque facts, not object-schema identities.
func (s *CatalogSnapshot) ValidateLayoutAuthoringReferences(
	document layoutauthoringv1.LayoutDocument,
	listFields []string,
	validateListFields bool,
) error {
	_, err := s.ResolveLayoutAuthoringReferences(document, listFields, validateListFields)
	return err
}

// ResolveLayoutAuthoringReferences validates the authored references and
// returns the exact Catalog object/field incarnations they resolve to. The
// result is complete, deterministic, and safe to use as a transaction-current
// persistence fence; callers must not re-resolve names through a second path.
func (s *CatalogSnapshot) ResolveLayoutAuthoringReferences(
	document layoutauthoringv1.LayoutDocument,
	listFields []string,
	validateListFields bool,
) ([]schema.IncarnationReference, error) {
	if s == nil {
		return nil, fmt.Errorf("Catalog snapshot is unavailable")
	}
	objects, err := s.layoutAuthoringObjects()
	if err != nil {
		return nil, err
	}
	return resolveLayoutAuthoringReferences(objects, s.doc.Relationships, document, listFields, validateListFields)
}

// ResolveLayoutAuthoringReferencesFromObjects applies the same neutral Layout
// reference rules to transaction-current schema rows. Runtime authoring uses
// this path so a normal Layout save does not depend on a Catalog artifact.
// Field ReferenceTo facts are sufficient for live schema; the additional
// immutable relationship records used by historical Catalog snapshots remain
// an artifact compatibility concern.
func ResolveLayoutAuthoringReferencesFromObjects(
	objects []*models.ObjectMetadata,
	document layoutauthoringv1.LayoutDocument,
	listFields []string,
	validateListFields bool,
	historyProjections ...layoutauthoringv2.HistoryRelatedListProjection,
) ([]schema.IncarnationReference, error) {
	byName := make(map[string]*models.ObjectMetadata, len(objects))
	for _, object := range objects {
		if object == nil || strings.TrimSpace(object.APIName) == "" {
			return nil, fmt.Errorf("current schema contains an invalid object")
		}
		key := foldIdentity(object.APIName)
		if _, duplicate := byName[key]; duplicate {
			return nil, fmt.Errorf("current schema object %q is ambiguous", object.APIName)
		}
		byName[key] = object
	}
	return resolveLayoutAuthoringReferences(byName, nil, document, listFields, validateListFields, historyProjections...)
}

type layoutAuthoringReferenceCollector struct {
	values map[string]schema.IncarnationReference
	err    error
}

func newLayoutAuthoringReferenceCollector() *layoutAuthoringReferenceCollector {
	return &layoutAuthoringReferenceCollector{values: make(map[string]schema.IncarnationReference)}
}

func (c *layoutAuthoringReferenceCollector) addObject(object *models.ObjectMetadata) {
	if object == nil || c.err != nil {
		return
	}
	id := strings.TrimSpace(object.ID)
	apiName := strings.TrimSpace(object.APIName)
	if id == "" || apiName == "" {
		c.err = fmt.Errorf("Catalog object %q has no exact canonical identity", object.APIName)
		return
	}
	reference := schema.IncarnationReference{ObjectID: id, ObjectAPIName: apiName}
	key := "object:" + foldIdentity(id)
	if existing, ok := c.values[key]; ok && !sameIncarnationReference(existing, reference) {
		c.err = fmt.Errorf("Catalog object identity %q is ambiguous", id)
		return
	}
	c.values[key] = reference
}

func (c *layoutAuthoringReferenceCollector) addField(object *models.ObjectMetadata, field *models.FieldMetadata) {
	if object == nil || field == nil || c.err != nil {
		return
	}
	c.addObject(object)
	if c.err != nil {
		return
	}
	c.addFieldIdentity(object, field)
	if c.err != nil || !models.ResolveFieldRelationshipSemantics(field, models.LifecyclePersistedRead).IsPolymorphic {
		return
	}
	companionAPIName := field.APIName + constants.PolymorphicTypeSuffix
	companion := models.FindField(object, companionAPIName)
	if companion == nil {
		c.err = fmt.Errorf(
			"polymorphic field %q.%s has no generated companion %s",
			object.APIName, field.APIName, companionAPIName,
		)
		return
	}
	c.addFieldIdentity(object, companion)
}

func (c *layoutAuthoringReferenceCollector) addFieldIdentity(object *models.ObjectMetadata, field *models.FieldMetadata) {
	fieldID := strings.TrimSpace(field.ID)
	fieldAPIName := strings.TrimSpace(field.APIName)
	if fieldID == "" || fieldAPIName == "" {
		c.err = fmt.Errorf("Catalog field %q.%s has no exact canonical identity", object.APIName, field.APIName)
		return
	}
	reference := schema.IncarnationReference{
		ObjectID: object.ID, ObjectAPIName: object.APIName,
		FieldID: fieldID, FieldAPIName: fieldAPIName,
	}
	key := "field:" + foldIdentity(fieldID)
	if existing, ok := c.values[key]; ok && !sameIncarnationReference(existing, reference) {
		c.err = fmt.Errorf("Catalog field identity %q is ambiguous", fieldID)
		return
	}
	c.values[key] = reference
}

func sameIncarnationReference(left, right schema.IncarnationReference) bool {
	return left.ObjectID == right.ObjectID && left.ObjectAPIName == right.ObjectAPIName &&
		left.FieldID == right.FieldID && left.FieldAPIName == right.FieldAPIName
}

func (c *layoutAuthoringReferenceCollector) sorted() ([]schema.IncarnationReference, error) {
	if c.err != nil {
		return nil, c.err
	}
	result := make([]schema.IncarnationReference, 0, len(c.values))
	for _, reference := range c.values {
		result = append(result, reference)
	}
	sort.Slice(result, func(i, j int) bool {
		if result[i].ObjectID != result[j].ObjectID {
			return result[i].ObjectID < result[j].ObjectID
		}
		return result[i].FieldID < result[j].FieldID
	})
	return result, nil
}

func resolveLayoutAuthoringReferences(
	objects map[string]*models.ObjectMetadata,
	relationships []relationshipRecord,
	document layoutauthoringv1.LayoutDocument,
	listFields []string,
	validateListFields bool,
	historyProjections ...layoutauthoringv2.HistoryRelatedListProjection,
) ([]schema.IncarnationReference, error) {
	if err := layoutauthoringv2.ValidateHistoryRelatedListProjections(document.ObjectID, historyProjections); err != nil {
		return nil, err
	}
	projections := make(map[string]layoutauthoringv2.HistoryRelatedListProjection, len(historyProjections))
	for _, projection := range historyProjections {
		projections[foldIdentity(projection.ObjectID)] = projection
	}
	collector := newLayoutAuthoringReferenceCollector()
	root := objects[foldIdentity(document.ObjectID)]
	if root == nil {
		return nil, fmt.Errorf("Layout object %q is absent from Catalog", document.ObjectID)
	}
	collector.addObject(root)
	for index, field := range document.CompactFields {
		if err := collectLayoutFieldPath(objects, relationships, root, field, collector); err != nil {
			return nil, fmt.Errorf("compact_fields[%d]: %w", index, err)
		}
	}
	for sectionIndex, section := range document.Sections {
		for fieldIndex, field := range section.Fields {
			if err := collectLayoutFieldPath(objects, relationships, root, field, collector); err != nil {
				return nil, fmt.Errorf("sections[%d].fields[%d]: %w", sectionIndex, fieldIndex, err)
			}
		}
	}
	for listIndex, list := range document.RelatedLists {
		if projection, derived := projections[foldIdentity(list.ObjectID)]; derived && objects[foldIdentity(list.ObjectID)] == nil {
			if err := validateHistoryRelatedList(list, projection); err != nil {
				return nil, fmt.Errorf("related_lists[%d]: %w", listIndex, err)
			}
			// All data comes from the root's existing history query. The root
			// incarnation is already collected; aliases have no database IDs.
			continue
		}
		child := objects[foldIdentity(list.ObjectID)]
		if child == nil {
			return nil, fmt.Errorf("related_lists[%d].object_id: object %q is absent from Catalog", listIndex, list.ObjectID)
		}
		collector.addObject(child)
		lookup := models.FindField(child, list.LookupFieldID)
		if lookup == nil {
			return nil, fmt.Errorf("related_lists[%d].lookup_field_id: field %q.%s is absent from Catalog", listIndex, child.APIName, list.LookupFieldID)
		}
		collector.addField(child, lookup)
		if !containsLayoutObject(layoutFieldReferenceTargets(relationships, child, lookup), root.APIName) {
			return nil, fmt.Errorf("related_lists[%d].lookup_field_id: field %q.%s does not reference %q", listIndex, child.APIName, lookup.APIName, root.APIName)
		}
		for fieldIndex, field := range list.FieldIDs {
			resolved := models.FindField(child, field)
			if resolved == nil {
				return nil, fmt.Errorf("related_lists[%d].field_ids[%d]: field %q.%s is absent from Catalog", listIndex, fieldIndex, child.APIName, field)
			}
			collector.addField(child, resolved)
		}
		for mappingIndex, mapping := range list.PrefillMappings {
			target := models.FindField(child, mapping.TargetFieldID)
			if target == nil {
				return nil, fmt.Errorf("related_lists[%d].prefill_mappings[%d].target_field_id: field %q.%s is absent from Catalog", listIndex, mappingIndex, child.APIName, mapping.TargetFieldID)
			}
			collector.addField(child, target)
			source := models.FindField(root, mapping.SourceFieldID)
			if source == nil {
				return nil, fmt.Errorf("related_lists[%d].prefill_mappings[%d].source_field_id: field %q.%s is absent from Catalog", listIndex, mappingIndex, root.APIName, mapping.SourceFieldID)
			}
			collector.addField(root, source)
		}
	}
	if err := collectLayoutActions(objects, "header_actions", document.HeaderActions, collector); err != nil {
		return nil, err
	}
	if err := collectLayoutActions(objects, "quick_actions", document.QuickActions, collector); err != nil {
		return nil, err
	}
	for ruleIndex, rule := range document.RecordStates {
		for linkIndex, link := range rule.Links {
			field := models.FindField(root, link.ReferenceFieldID)
			if field == nil {
				return nil, fmt.Errorf("record_states[%d].links[%d].reference_field_id: field %q.%s is absent from Catalog", ruleIndex, linkIndex, root.APIName, link.ReferenceFieldID)
			}
			collector.addField(root, field)
			if link.ObjectID == "" {
				continue
			}
			target := objects[foldIdentity(link.ObjectID)]
			if target == nil {
				return nil, fmt.Errorf("record_states[%d].links[%d].object_id: object %q is absent from Catalog", ruleIndex, linkIndex, link.ObjectID)
			}
			collector.addObject(target)
			if !containsLayoutObject(layoutFieldReferenceTargets(relationships, root, field), link.ObjectID) {
				return nil, fmt.Errorf("record_states[%d].links[%d]: field %q.%s does not reference %q", ruleIndex, linkIndex, root.APIName, field.APIName, link.ObjectID)
			}
		}
	}
	if validateListFields {
		for index, field := range listFields {
			resolved := models.FindField(root, field)
			if resolved == nil {
				return nil, fmt.Errorf("list_fields[%d]: field %q.%s is absent from Catalog", index, root.APIName, field)
			}
			collector.addField(root, resolved)
		}
	}
	return collector.sorted()
}

func (s *CatalogSnapshot) layoutAuthoringObjects() (map[string]*models.ObjectMetadata, error) {
	objects := make(map[string]*models.ObjectMetadata, len(s.doc.Objects))
	for index := range s.doc.Objects {
		object, err := reconstructCatalogObject(s.doc.Objects[index])
		if err != nil {
			return nil, fmt.Errorf("reconstruct Catalog object: %w", err)
		}
		objects[foldIdentity(object.APIName)] = object
	}
	return objects, nil
}

func collectLayoutFieldPath(
	objects map[string]*models.ObjectMetadata,
	relationships []relationshipRecord,
	object *models.ObjectMetadata,
	path string,
	collector *layoutAuthoringReferenceCollector,
) error {
	segments := strings.Split(path, ".")
	_, references, err := resolveLayoutFieldSegments(objects, relationships, object, segments, path)
	if err != nil {
		return err
	}
	for _, reference := range references {
		collector.addField(reference.object, reference.field)
	}
	return collector.err
}

type resolvedLayoutFieldReference struct {
	object *models.ObjectMetadata
	field  *models.FieldMetadata
}

// resolveLayoutFieldSegments collects every successful polymorphic traversal,
// not only the first branch. A mutation must fence all identities the immutable
// Catalog admits for the authored path.
func resolveLayoutFieldSegments(
	objects map[string]*models.ObjectMetadata,
	relationships []relationshipRecord,
	object *models.ObjectMetadata,
	segments []string,
	fullPath string,
) (*models.FieldMetadata, []resolvedLayoutFieldReference, error) {
	field := models.FindField(object, segments[0])
	if field == nil {
		return nil, nil, fmt.Errorf("field path %q is absent at %q.%s", fullPath, object.APIName, segments[0])
	}
	references := []resolvedLayoutFieldReference{{object: object, field: field}}
	if len(segments) == 1 {
		return field, references, nil
	}
	var resolvedField *models.FieldMetadata
	resolvedReferences := append([]resolvedLayoutFieldReference(nil), references...)
	for _, target := range layoutFieldReferenceTargets(relationships, object, field) {
		next := objects[foldIdentity(target)]
		if next == nil {
			continue
		}
		resolved, nested, err := resolveLayoutFieldSegments(objects, relationships, next, segments[1:], fullPath)
		if err != nil {
			continue
		}
		if resolvedField == nil {
			resolvedField = resolved
		}
		resolvedReferences = append(resolvedReferences, nested...)
	}
	if resolvedField != nil {
		return resolvedField, resolvedReferences, nil
	}
	return nil, nil, fmt.Errorf("field path %q cannot traverse %q.%s", fullPath, object.APIName, field.APIName)
}

func layoutFieldReferenceTargets(
	relationships []relationshipRecord,
	object *models.ObjectMetadata,
	field *models.FieldMetadata,
) []string {
	targets := append([]string(nil), field.ReferenceTo...)
	for _, relationship := range relationships {
		if strings.EqualFold(relationship.ChildObjectAPIName, object.APIName) &&
			strings.EqualFold(relationship.FieldAPIName, field.APIName) &&
			!containsLayoutObject(targets, relationship.ParentObjectAPIName) {
			targets = append(targets, relationship.ParentObjectAPIName)
		}
	}
	return targets
}

func collectLayoutActions(
	objects map[string]*models.ObjectMetadata,
	path string,
	placement layoutauthoringv1.ActionPlacement,
	collector *layoutAuthoringReferenceCollector,
) error {
	for index, action := range placement.Actions {
		if action.TargetObjectID == nil || *action.TargetObjectID == "" {
			continue
		}
		target := objects[foldIdentity(*action.TargetObjectID)]
		if target == nil {
			return fmt.Errorf("%s.actions[%d].target_object_id: object %q is absent from Catalog", path, index, *action.TargetObjectID)
		}
		collector.addObject(target)
	}
	return collector.err
}

func containsLayoutObject(values []string, target string) bool {
	for _, value := range values {
		if strings.EqualFold(value, target) {
			return true
		}
	}
	return false
}

func validateHistoryRelatedList(list layoutauthoringv1.RelatedList, projection layoutauthoringv2.HistoryRelatedListProjection) error {
	columns := make(map[string]historysemantic.HistoryColumn, len(projection.Columns))
	for name, member := range projection.Columns {
		columns[foldIdentity(name)] = member
	}
	if columns[foldIdentity(list.LookupFieldID)] != historysemantic.HistoryColumnParentID {
		return fmt.Errorf("lookup_field_id: field %q.%s is not the history parent reference", list.ObjectID, list.LookupFieldID)
	}
	for index, name := range list.FieldIDs {
		if _, found := columns[foldIdentity(name)]; !found {
			return fmt.Errorf("field_ids[%d]: field %q.%s is absent from the history result", index, list.ObjectID, name)
		}
	}
	if len(list.PrefillMappings) != 0 {
		return fmt.Errorf("prefill_mappings: record history is read-only")
	}
	return nil
}
