package catalogsnapshot

import (
	"fmt"
	"sort"
	"strings"

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

func inputHasRecordViewProjection(values []RuntimeProjectionInput) bool {
	for _, value := range values {
		if value.Profile == ProjectionRecordViewV1 {
			return true
		}
	}
	return false
}

func buildRecordViewDefinitionDocument(
	revision CatalogRevision,
	resolverInputs []RecordAttachmentSourceResolverInput,
	fieldInputs []RecordAttachmentSourceFieldInput,
	objects map[string]*models.ObjectMetadata,
) (*recordViewDefinitionDocument, error) {
	if len(resolverInputs) == 0 && len(fieldInputs) == 0 {
		return nil, nil
	}
	doc := &recordViewDefinitionDocument{
		Resolvers: []recordAttachmentSourceResolverRecord{},
		Fields:    []recordAttachmentSourceFieldRecord{},
	}
	resolvers := make(map[string]recordAttachmentSourceResolverRecord, len(resolverInputs))
	for i, input := range resolverInputs {
		path := fmt.Sprintf("attachmentSourceResolvers[%d]", i)
		if err := requireSourceRevision(path, revision, input.CatalogRevision); err != nil {
			return nil, err
		}
		for name, value := range map[string]string{
			"resolverId":                      input.ResolverID,
			"sourceObjectApiName":             input.SourceObjectAPIName,
			"sourceFileReferenceFieldApiName": input.SourceFileReferenceFieldAPIName,
		} {
			if err := requireCanonicalIdentity(path+"."+name, value); err != nil {
				return nil, err
			}
		}
		if input.SourceDeletedFieldAPIName != "" {
			if err := requireCanonicalIdentity(path+".sourceDeletedFieldApiName", input.SourceDeletedFieldAPIName); err != nil {
				return nil, err
			}
		}
		if input.Kind != RecordAttachmentSourceByRecordV2 {
			return nil, conflict(ConflictInvalidAttachmentSource, path+".kind", "unsupported resolver kind %q", input.Kind)
		}
		matcher, err := buildRecordAttachmentSourceMatcher(path+".sourceRecordMatcher", input.SourceRecordMatcher)
		if err != nil {
			return nil, err
		}
		key := foldIdentity(input.ResolverID)
		if _, exists := resolvers[key]; exists {
			return nil, conflict(ConflictInvalidAttachmentSource, path+".resolverId", "resolver %q is duplicated case-insensitively", input.ResolverID)
		}
		record := recordAttachmentSourceResolverRecord{
			ResolverID: input.ResolverID, Kind: input.Kind,
			SourceObjectAPIName:             input.SourceObjectAPIName,
			SourceFileReferenceFieldAPIName: input.SourceFileReferenceFieldAPIName,
			SourceDeletedFieldAPIName:       input.SourceDeletedFieldAPIName,
			SourceRecordMatcher:             matcher,
		}
		resolvers[key] = record
		doc.Resolvers = append(doc.Resolvers, record)
	}
	sort.Slice(doc.Resolvers, func(i, j int) bool {
		return identityLess(doc.Resolvers[i].ResolverID, doc.Resolvers[j].ResolverID)
	})

	seenFields := make(map[string]struct{}, len(fieldInputs))
	for i, input := range fieldInputs {
		path := fmt.Sprintf("attachmentSourceFields[%d]", i)
		if err := requireSourceRevision(path, revision, input.CatalogRevision); err != nil {
			return nil, err
		}
		for name, value := range map[string]string{
			"objectApiName": input.ObjectAPIName,
			"fieldApiName":  input.FieldAPIName,
			"resolverId":    input.ResolverID,
		} {
			if err := requireCanonicalIdentity(path+"."+name, value); err != nil {
				return nil, err
			}
		}
		if _, exists := resolvers[foldIdentity(input.ResolverID)]; !exists {
			return nil, conflict(ConflictInvalidAttachmentSource, path+".resolverId", "resolver %q is absent", input.ResolverID)
		}
		object := objects[foldIdentity(input.ObjectAPIName)]
		if object == nil {
			return nil, conflict(ConflictInvalidAttachmentSource, path+".objectApiName", "object %q is absent", input.ObjectAPIName)
		}
		field := models.FindField(object, input.FieldAPIName)
		if field == nil {
			return nil, conflict(ConflictInvalidAttachmentSource, path+".fieldApiName", "field %q is absent from object %q", input.FieldAPIName, input.ObjectAPIName)
		}
		if field.IsVirtual || field.IsCompound || !attachmentSourceStringField(*field) {
			return nil, conflict(ConflictInvalidAttachmentSource, path+".fieldApiName", "field %q on object %q is not a scalar string field", input.FieldAPIName, input.ObjectAPIName)
		}
		key := foldIdentity(input.ObjectAPIName) + "\x00" + foldIdentity(input.FieldAPIName)
		if _, exists := seenFields[key]; exists {
			return nil, conflict(ConflictInvalidAttachmentSource, path, "attachment source field %q.%s is duplicated case-insensitively", input.ObjectAPIName, input.FieldAPIName)
		}
		seenFields[key] = struct{}{}
		doc.Fields = append(doc.Fields, recordAttachmentSourceFieldRecord{
			ObjectAPIName: object.APIName, FieldAPIName: field.APIName, ResolverID: resolvers[foldIdentity(input.ResolverID)].ResolverID,
		})
	}
	sort.Slice(doc.Fields, func(i, j int) bool {
		left := foldIdentity(doc.Fields[i].ObjectAPIName) + "\x00" + foldIdentity(doc.Fields[i].FieldAPIName)
		right := foldIdentity(doc.Fields[j].ObjectAPIName) + "\x00" + foldIdentity(doc.Fields[j].FieldAPIName)
		if left == right {
			return doc.Fields[i].ObjectAPIName+"\x00"+doc.Fields[i].FieldAPIName < doc.Fields[j].ObjectAPIName+"\x00"+doc.Fields[j].FieldAPIName
		}
		return left < right
	})
	return doc, nil
}

func attachmentSourceStringField(field models.FieldMetadata) bool {
	switch schemautil.EffectiveFieldType(field) {
	case constants.FieldTypeString, constants.FieldTypePicklist, constants.FieldTypeEmail,
		constants.FieldTypePhone, constants.FieldTypeTextarea, constants.FieldTypeUrl,
		constants.FieldTypeRichtextarea, constants.FieldTypeAutonumber,
		constants.FieldTypeEncryptedString, constants.FieldTypePassword:
		return true
	default:
		return false
	}
}

func buildRecordAttachmentSourceMatcher(path string, input *RecordAttachmentSourceRecordMatcher) (*recordAttachmentSourceMatcherRecord, error) {
	if input == nil {
		return nil, conflict(ConflictInvalidAttachmentSource, path, "record-view v2 resolver requires a source-record matcher")
	}
	matcher := &recordAttachmentSourceMatcherRecord{
		Kind: input.Kind, StartOffsetBytes: clonePointer(input.StartOffsetBytes), LengthBytes: input.LengthBytes,
	}
	// A historical length-only segment remains a valid immutable release fact
	// so Catalog can reconstruct existing release lifecycle targets. It has no
	// serving authority; AttachmentSourceResolver filters it below.
	if err := validateRecordAttachmentSourceMatcher(path, matcher); err != nil {
		return nil, err
	}
	return matcher, nil
}

func validateRecordAttachmentSourceMatcher(path string, matcher *recordAttachmentSourceMatcherRecord) error {
	if matcher == nil {
		return fmt.Errorf("%s is required", path)
	}
	switch matcher.Kind {
	case RecordAttachmentSourceMatchRecordIDV1:
		if matcher.StartOffsetBytes != nil || matcher.LengthBytes != 0 {
			return fmt.Errorf("%s exact matcher must not declare a segment", path)
		}
	case RecordAttachmentSourceMatchRecordIDSegmentV1:
		if matcher.StartOffsetBytes != nil {
			return fmt.Errorf("%s historical segment matcher must not declare a start offset", path)
		}
		if matcher.LengthBytes == 0 {
			return fmt.Errorf("%s segment matcher requires a non-zero length", path)
		}
		if int(matcher.LengthBytes) > maxAttachmentSourceRecordIdentifierBytes {
			return fmt.Errorf("%s segment exceeds the %d-byte record identifier contract", path, maxAttachmentSourceRecordIdentifierBytes)
		}
	case RecordAttachmentSourceMatchRecordIDSegmentV2:
		if matcher.StartOffsetBytes == nil {
			return fmt.Errorf("%s segment matcher requires a start offset", path)
		}
		if matcher.LengthBytes == 0 {
			return fmt.Errorf("%s segment matcher requires a non-zero length", path)
		}
		if int(*matcher.StartOffsetBytes)+int(matcher.LengthBytes) > maxAttachmentSourceRecordIdentifierBytes {
			return fmt.Errorf("%s segment exceeds the %d-byte record identifier contract", path, maxAttachmentSourceRecordIdentifierBytes)
		}
	default:
		return fmt.Errorf("%s has unsupported matcher kind %q", path, matcher.Kind)
	}
	return nil
}

func validateServingRecordAttachmentSourceMatcher(path string, matcher *recordAttachmentSourceMatcherRecord) error {
	if err := validateRecordAttachmentSourceMatcher(path, matcher); err != nil {
		return err
	}
	if matcher.Kind == RecordAttachmentSourceMatchRecordIDSegmentV1 {
		return fmt.Errorf("%s historical segment matcher lacks a static source-record start offset", path)
	}
	return nil
}

func recordAttachmentSourceMatcherServes(matcher *recordAttachmentSourceMatcherRecord) bool {
	if matcher == nil || validateServingRecordAttachmentSourceMatcher("sourceRecordMatcher", matcher) != nil {
		return false
	}
	return matcher.Kind == RecordAttachmentSourceMatchRecordIDV1 || matcher.Kind == RecordAttachmentSourceMatchRecordIDSegmentV2
}

func validateRecordViewDefinitions(doc *recordViewDefinitionDocument, objects map[string]*models.ObjectMetadata, schema string) error {
	if doc == nil {
		return nil
	}
	resolvers := make(map[string]struct{}, len(doc.Resolvers))
	lastResolver := ""
	for i, resolver := range doc.Resolvers {
		path := fmt.Sprintf("recordView.attachmentSourceResolvers[%d]", i)
		for name, value := range map[string]string{
			"resolverId": resolver.ResolverID, "sourceObjectApiName": resolver.SourceObjectAPIName,
			"sourceFileReferenceFieldApiName": resolver.SourceFileReferenceFieldAPIName,
		} {
			if err := requireCanonicalIdentity(path+"."+name, value); err != nil {
				return err
			}
		}
		if resolver.SourceDeletedFieldAPIName != "" {
			if err := requireCanonicalIdentity(path+".sourceDeletedFieldApiName", resolver.SourceDeletedFieldAPIName); err != nil {
				return err
			}
		}
		switch schema {
		case CatalogSnapshotSchemaV5:
			// V5 remains decodable solely for historical non-serving artifact
			// verification. New compilation always produces V6.
			if resolver.Kind != RecordAttachmentSourceByRecordV1 || resolver.SourceRecordMatcher != nil {
				return fmt.Errorf("%s is not a canonical v5 resolver", path)
			}
		case CatalogSnapshotSchemaV6, CatalogSnapshotSchemaV8, CatalogSnapshotSchemaV9, CatalogSnapshotSchemaV10, CatalogSnapshotSchemaV11, CatalogSnapshotSchemaV12, CatalogSnapshotSchemaV13, CatalogSnapshotSchemaV14, CatalogSnapshotSchemaV15, CatalogSnapshotSchemaV16, CatalogSnapshotSchemaV17, CatalogSnapshotSchemaV18, CatalogSnapshotSchemaV19, CatalogSnapshotSchemaV20, CatalogSnapshotSchemaV21:
			if resolver.Kind != RecordAttachmentSourceByRecordV2 {
				return fmt.Errorf("%s has unsupported v6 kind %q", path, resolver.Kind)
			}
			if err := validateRecordAttachmentSourceMatcher(path+".sourceRecordMatcher", resolver.SourceRecordMatcher); err != nil {
				return err
			}
		default:
			return fmt.Errorf("%s has record-view resolver under unsupported schema %q", path, schema)
		}
		key := foldIdentity(resolver.ResolverID)
		if _, duplicate := resolvers[key]; duplicate || (lastResolver != "" && !identityLess(lastResolver, resolver.ResolverID)) {
			return fmt.Errorf("%s is duplicated or not canonically ordered", path)
		}
		resolvers[key] = struct{}{}
		lastResolver = resolver.ResolverID
	}
	seenFields := map[string]struct{}{}
	lastField := ""
	for i, binding := range doc.Fields {
		path := fmt.Sprintf("recordView.attachmentSourceFields[%d]", i)
		if _, exists := resolvers[foldIdentity(binding.ResolverID)]; !exists {
			return fmt.Errorf("%s references absent resolver %q", path, binding.ResolverID)
		}
		object := objects[foldIdentity(binding.ObjectAPIName)]
		if object == nil {
			return fmt.Errorf("%s references absent object %q", path, binding.ObjectAPIName)
		}
		field := models.FindField(object, binding.FieldAPIName)
		if field == nil || field.IsVirtual || field.IsCompound || !attachmentSourceStringField(*field) {
			return fmt.Errorf("%s references a missing or non-string scalar field %q", path, binding.FieldAPIName)
		}
		key := foldIdentity(binding.ObjectAPIName) + "\x00" + foldIdentity(binding.FieldAPIName)
		if _, duplicate := seenFields[key]; duplicate || (lastField != "" && strings.Compare(lastField, key) >= 0) {
			return fmt.Errorf("%s is duplicated or not canonically ordered", path)
		}
		seenFields[key] = struct{}{}
		lastField = key
	}
	return nil
}

func projectRecordViewDefinitions(source *recordViewDefinitionDocument, included map[string]struct{}) *recordViewDefinitionDocument {
	if source == nil {
		return nil
	}
	projected := &recordViewDefinitionDocument{
		Resolvers: append([]recordAttachmentSourceResolverRecord{}, source.Resolvers...),
		Fields:    []recordAttachmentSourceFieldRecord{},
	}
	usedResolvers := map[string]struct{}{}
	for _, field := range source.Fields {
		if _, ok := included[foldIdentity(field.ObjectAPIName)]; !ok {
			continue
		}
		projected.Fields = append(projected.Fields, field)
		usedResolvers[foldIdentity(field.ResolverID)] = struct{}{}
	}
	filtered := projected.Resolvers[:0]
	for _, resolver := range projected.Resolvers {
		if _, used := usedResolvers[foldIdentity(resolver.ResolverID)]; used {
			filtered = append(filtered, resolver)
		}
	}
	projected.Resolvers = filtered
	return projected
}

func (v *RuntimeMetadataView) AttachmentSourceResolver(resolverID string) (RecordAttachmentSourceResolver, bool) {
	if !v.SupportsRecordViewServing() {
		return RecordAttachmentSourceResolver{}, false
	}
	index, ok := v.attachmentResolversByID[foldIdentity(resolverID)]
	if !ok || v.doc.RecordView == nil {
		return RecordAttachmentSourceResolver{}, false
	}
	record := v.doc.RecordView.Resolvers[index]
	if !recordAttachmentSourceMatcherServes(record.SourceRecordMatcher) {
		return RecordAttachmentSourceResolver{}, false
	}
	return RecordAttachmentSourceResolver{
		ResolverID: record.ResolverID, Kind: record.Kind,
		SourceObjectAPIName:             record.SourceObjectAPIName,
		SourceFileReferenceFieldAPIName: record.SourceFileReferenceFieldAPIName,
		SourceDeletedFieldAPIName:       record.SourceDeletedFieldAPIName,
		SourceRecordMatcher: func() *RecordAttachmentSourceRecordMatcher {
			if record.SourceRecordMatcher == nil {
				return nil
			}
			return &RecordAttachmentSourceRecordMatcher{
				Kind: record.SourceRecordMatcher.Kind, StartOffsetBytes: clonePointer(record.SourceRecordMatcher.StartOffsetBytes), LengthBytes: record.SourceRecordMatcher.LengthBytes,
			}
		}(),
	}, true
}

func (v *RuntimeMetadataView) AttachmentSourceFieldsForObject(objectAPIName string) []RecordAttachmentSourceField {
	if !v.SupportsRecordViewServing() {
		return nil
	}
	indexes := v.attachmentFieldsByObject[foldIdentity(objectAPIName)]
	result := make([]RecordAttachmentSourceField, 0, len(indexes))
	for _, index := range indexes {
		record := v.doc.RecordView.Fields[index]
		if _, serving := v.AttachmentSourceResolver(record.ResolverID); !serving {
			continue
		}
		result = append(result, RecordAttachmentSourceField{
			ObjectAPIName: record.ObjectAPIName, FieldAPIName: record.FieldAPIName, ResolverID: record.ResolverID,
		})
	}
	return result
}

// SupportsRecordViewServing reports whether this immutable projection has the
// complete record-view grammar required to serve a request. Historical V5-V7
// views remain readable for audit, upgrade, and rollback verification, but
// they must never silently serve a view without the Catalog-declared display
// field and access-decision closure completed by V24.
func (v *RuntimeMetadataView) SupportsRecordViewServing() bool {
	return v != nil && v.doc.Schema == RuntimeMetadataViewSchemaV24 &&
		v.doc.Profile == ProjectionRecordViewV1 && v.doc.RecordView != nil
}

// SupportsRecordAccessServing reports whether this immutable projection
// carries the object identity closure required to resolve prefix-addressed
// record IDs without probing physical tables. Historical record-access views
// remain readable, but cannot serve because they omit KeyPrefix.
func (v *RuntimeMetadataView) SupportsRecordAccessServing() bool {
	return v != nil && v.doc.Schema == RuntimeMetadataViewSchemaV8 &&
		v.doc.Profile == ProjectionRecordAccessV1
}
