package packs

import (
	"context"
	"fmt"
	"strings"

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

type ObjectMetadataSnapshot struct {
	Index    map[string]*models.ObjectMetadata
	Fallback records.DefaultChildRowsSchemaSnapshot
}

func NewObjectMetadataSnapshot(specs []ObjectSpec, fallback records.DefaultChildRowsSchemaSnapshot) ObjectMetadataSnapshot {
	return ObjectMetadataSnapshot{
		Index:    BuildObjectMetadataIndex(specs),
		Fallback: fallback,
	}
}

func (s ObjectMetadataSnapshot) GetSchema(ctx context.Context, apiName string) *models.ObjectMetadata {
	if s.Index != nil {
		if schema := s.Index[strings.ToLower(strings.TrimSpace(apiName))]; schema != nil {
			return schema
		}
	}
	if s.Fallback != nil {
		return s.Fallback.GetSchema(ctx, apiName)
	}
	return nil
}

// BuildObjectMetadataIndex converts ObjectSpecs to a case-insensitive
// APIName→ObjectMetadata map.  When the same APIName appears in more than one
// ObjectSpec (a real scenario — see child_relationship_validation.go for
// split-spec usage), the Fields slices are unioned: any field present in
// either spec is reachable in the resulting entry.  This mirrors the semantics
// of buildFlowObjectFieldGraph (flow_field_reference_validation.go line 20:
// "Fields are unioned across multiple specs"), so that filterFormula Tier-3
// validation does not false-reject a field declared in an earlier spec.
func BuildObjectMetadataIndex(specs []ObjectSpec) map[string]*models.ObjectMetadata {
	objects := buildObjectMetadata(specs)
	index := make(map[string]*models.ObjectMetadata, len(objects))
	for _, obj := range objects {
		if obj == nil {
			continue
		}
		key := strings.ToLower(strings.TrimSpace(obj.APIName))
		if existing, ok := index[key]; ok {
			index[key] = unionObjectMetadataFields(existing, obj)
		} else {
			index[key] = obj
		}
	}
	return index
}

// unionObjectMetadataFields returns a new ObjectMetadata that is a shallow
// copy of a with its Fields slice extended by any field from b whose APIName
// (case-insensitive) is not already present in a.  Neither a nor b is mutated.
func unionObjectMetadataFields(a, b *models.ObjectMetadata) *models.ObjectMetadata {
	present := make(map[string]struct{}, len(a.Fields))
	for _, f := range a.Fields {
		present[strings.ToLower(f.APIName)] = struct{}{}
	}
	merged := append([]models.FieldMetadata(nil), a.Fields...)
	for _, f := range b.Fields {
		if _, exists := present[strings.ToLower(f.APIName)]; !exists {
			merged = append(merged, f)
		}
	}
	copied := *a
	copied.Fields = merged
	return &copied
}

// augmentObjectMetadataWithPlatformFields returns a shallow copy of obj with
// the platform-owned fields (Id, OwnerId, CreatedDate, etc.) appended to
// Fields if they are not already present.  For custom objects RecordTypeId is
// also added.  The original obj is not mutated.
func augmentObjectMetadataWithPlatformFields(obj *models.ObjectMetadata) *models.ObjectMetadata {
	// Build a set of already-present field names for O(1) dedup.
	present := make(map[string]struct{}, len(obj.Fields))
	for _, f := range obj.Fields {
		present[f.APIName] = struct{}{}
	}

	// Platform fields expressed directly here so we can attach the correct type
	// metadata.  Reference-typed fields carry ReferenceTo and RelationshipName to
	// match the live-DB path (schema_system_columns.go:300-327), which is required
	// for resolveSchemaRelationRoot to traverse relationship paths such as
	// $Record.Owner.Name during filterFormula Tier-3 validation.
	type platformField struct {
		apiName          string
		typ              constants.SchemaFieldType
		referenceTo      []string
		relationshipName string
	}
	base := []platformField{
		{constants.FieldID, constants.FieldTypeString, nil, ""},
		{constants.FieldOwnerID, constants.FieldTypeReference, []string{constants.TableUser}, "Owner"},
		{constants.FieldCreatedDate, constants.FieldTypeDatetime, nil, ""},
		{constants.FieldCreatedByID, constants.FieldTypeReference, []string{constants.TableUser}, "CreatedBy"},
		{constants.FieldLastModifiedDate, constants.FieldTypeDatetime, nil, ""},
		{constants.FieldLastModifiedByID, constants.FieldTypeReference, []string{constants.TableUser}, "LastModifiedBy"},
		{constants.FieldIsDeleted, constants.FieldTypeBoolean, nil, ""},
		{constants.FieldSystemModstamp, constants.FieldTypeDatetime, nil, ""},
	}
	if obj.TableType == constants.TableTypeCustomObject {
		var rtReferenceTo []string
		if target, ok := constants.PlatformReferenceTargetForField(constants.FieldRecordTypeID); ok {
			rtReferenceTo = []string{target}
		}
		base = append(base, platformField{
			apiName:          constants.FieldRecordTypeID,
			typ:              constants.FieldTypeReference,
			referenceTo:      rtReferenceTo,
			relationshipName: constants.ObjectRecordType,
		})
	}

	var extra []models.FieldMetadata
	for _, pf := range base {
		if _, exists := present[pf.apiName]; !exists {
			f := models.FieldMetadata{
				APIName: pf.apiName,
				Type:    pf.typ,
			}
			if len(pf.referenceTo) > 0 {
				f.ReferenceTo = pf.referenceTo
			}
			if pf.relationshipName != "" {
				relName := pf.relationshipName
				f.RelationshipName = &relName
			}
			extra = append(extra, f)
		}
	}
	if len(extra) == 0 {
		return obj // nothing to add; return as-is
	}

	// Shallow-copy obj so the original (held by other callers) is not mutated.
	copied := *obj
	merged := make([]models.FieldMetadata, len(obj.Fields), len(obj.Fields)+len(extra))
	copy(merged, obj.Fields)
	merged = append(merged, extra...)
	copied.Fields = merged
	return &copied
}

func DefaultChildRowsEngineOwnedByObject(specs []ObjectSpec, fallback func(string) map[string]bool) func(string) map[string]bool {
	index := BuildObjectMetadataIndex(specs)
	return func(objectName string) map[string]bool {
		owned := map[string]bool{}
		if fallback != nil {
			for key, value := range fallback(objectName) {
				if value {
					owned[strings.ToLower(key)] = true
				}
			}
		}
		if schema := index[strings.ToLower(strings.TrimSpace(objectName))]; schema != nil {
			for i := range schema.Fields {
				field := &schema.Fields[i]
				if constants.NormalizeFieldType(string(field.Type)) == constants.FieldTypeAutonumber {
					owned[strings.ToLower(field.APIName)] = true
				}
			}
		}
		if len(owned) == 0 {
			return nil
		}
		return owned
	}
}

func DefaultChildRowsEngineOwnedByAutoNumberSeeds(seeds []AutoNumberFormatSeed) func(string) map[string]bool {
	if len(seeds) == 0 {
		return nil
	}
	index := make(map[string]map[string]bool)
	for _, seed := range seeds {
		objectName := strings.ToLower(strings.TrimSpace(seed.ObjectName))
		fieldAPIName := strings.ToLower(strings.TrimSpace(seed.FieldApiName))
		if objectName == "" || fieldAPIName == "" {
			continue
		}
		fields := index[objectName]
		if fields == nil {
			fields = map[string]bool{}
			index[objectName] = fields
		}
		fields[fieldAPIName] = true
	}
	if len(index) == 0 {
		return nil
	}
	return func(objectName string) map[string]bool {
		fields := index[strings.ToLower(strings.TrimSpace(objectName))]
		if len(fields) == 0 {
			return nil
		}
		out := make(map[string]bool, len(fields))
		for key, value := range fields {
			if value {
				out[key] = true
			}
		}
		return out
	}
}

func MergeDefaultChildRowsEngineOwnedLookups(lookups ...func(string) map[string]bool) func(string) map[string]bool {
	var filtered []func(string) map[string]bool
	for _, lookup := range lookups {
		if lookup != nil {
			filtered = append(filtered, lookup)
		}
	}
	if len(filtered) == 0 {
		return nil
	}
	return func(objectName string) map[string]bool {
		merged := map[string]bool{}
		for _, lookup := range filtered {
			for key, value := range lookup(objectName) {
				if value {
					merged[strings.ToLower(key)] = true
				}
			}
		}
		if len(merged) == 0 {
			return nil
		}
		return merged
	}
}

func DefaultChildRowsEngineOwnedByResolvedSource(source *ResolvedPackSource) (func(string) map[string]bool, error) {
	formats, err := sourceBackedAutoNumberFormatSeedsFromResolvedSource(source)
	if err != nil {
		return nil, err
	}
	return DefaultChildRowsEngineOwnedByAutoNumberSeeds(formats), nil
}

func DefaultChildRowsEngineOwnedBySourcePack(pack *SourceBackedPack) (func(string) map[string]bool, error) {
	formats, err := sourceBackedAutoNumberFormatSeedsFromSourcePack(pack)
	if err != nil {
		return nil, err
	}
	return DefaultChildRowsEngineOwnedByAutoNumberSeeds(formats), nil
}

func validateInstallableSourceBackedDefaultChildRows(source *ResolvedPackSource) []PackDiagnostic {
	specs, err := collectResolvedObjectSpecs(source)
	if err != nil {
		return []PackDiagnostic{diag("component.payload.default_child_rows.invalid", "components", fmt.Sprintf("collect object specs for defaultChildRows validation: %v", err))}
	}
	if len(specs) == 0 {
		return nil
	}
	engineOwned, err := DefaultChildRowsEngineOwnedByResolvedSource(source)
	if err != nil {
		return []PackDiagnostic{diag("component.payload.default_child_rows.invalid", "components", fmt.Sprintf("collect autonumber ownership for defaultChildRows validation: %v", err))}
	}
	if err := ValidateDefaultChildRowsForSpecs(context.Background(), specs, nil, engineOwned, true); err != nil {
		return []PackDiagnostic{diag("component.payload.default_child_rows.invalid", "components", err.Error())}
	}
	return nil
}

func validateInstallableSourceBackedChildCloneRules(source *ResolvedPackSource) []PackDiagnostic {
	if source == nil || source.Manifest == nil {
		return nil
	}
	var diagnostics []PackDiagnostic
	parsedObjectSpecs := make(map[string][]ObjectSpec)
	objectSpecIndex := make(map[string]sourceBackedObjectSpecTarget)
	objectSpecBusinessKeys := make([]string, 0)
	hasChildCloneRulesComponent := false
	for i, component := range source.Manifest.Components {
		if component.Kind == ComponentKindChildCloneRules {
			hasChildCloneRulesComponent = true
		}
		if component.Kind != ComponentKindObjectSpec {
			continue
		}
		payload, err := sourceBackedComponentPayload(source, component)
		if err != nil {
			diagnostics = append(diagnostics, diag(
				"component.payload.child_clone_rules.invalid",
				fmt.Sprintf("components[%d]", i),
				fmt.Sprintf("collect object specs for childCloneRules validation: %v", err),
			))
			continue
		}
		specs, err := parseObjectSpecsJSON(payload, componentPayloadLabel(component))
		if err != nil {
			diagnostics = append(diagnostics, diag(
				"component.payload.child_clone_rules.invalid",
				fmt.Sprintf("components[%d]", i),
				fmt.Sprintf("collect object specs for childCloneRules validation: %v", err),
			))
			continue
		}
		businessKey := strings.TrimSpace(component.BusinessKey)
		parsedObjectSpecs[businessKey] = specs
		objectSpecBusinessKeys = append(objectSpecBusinessKeys, businessKey)
		for _, spec := range specs {
			objectAPIName := strings.ToLower(strings.TrimSpace(spec.APIName))
			if objectAPIName == "" {
				continue
			}
			objectSpecIndex[objectAPIName] = sourceBackedObjectSpecTarget{
				businessKey:   businessKey,
				manageability: component.Manageability,
				objectAPIName: strings.TrimSpace(spec.APIName),
			}
		}
	}
	if len(parsedObjectSpecs) == 0 && !hasChildCloneRulesComponent {
		return diagnostics
	}
	if _, err := overlaySourceBackedChildCloneRules(source, source.Manifest.Components, parsedObjectSpecs, objectSpecIndex); err != nil {
		diagnostics = append(diagnostics, diag("component.payload.child_clone_rules.invalid", "components", err.Error()))
		return diagnostics
	}
	allSpecs := make([]ObjectSpec, 0)
	for _, businessKey := range objectSpecBusinessKeys {
		allSpecs = append(allSpecs, parsedObjectSpecs[businessKey]...)
	}
	if err := ValidateChildCloneRulesForSpecs(context.Background(), allSpecs, nil, true); err != nil {
		diagnostics = append(diagnostics, diag("component.payload.child_clone_rules.invalid", "components", err.Error()))
	}
	return diagnostics
}

func collectResolvedObjectSpecs(source *ResolvedPackSource) ([]ObjectSpec, error) {
	if source == nil || source.Manifest == nil {
		return nil, nil
	}
	var specs []ObjectSpec
	for _, component := range source.Manifest.Components {
		if component.Kind != ComponentKindObjectSpec {
			continue
		}
		payload, err := sourceBackedComponentPayload(source, component)
		if err != nil {
			return nil, err
		}
		parsed, err := parseObjectSpecsJSON(payload, componentPayloadLabel(component))
		if err != nil {
			return nil, err
		}
		specs = append(specs, parsed...)
	}
	return specs, nil
}

func ParseObjectSpecsPayloads(payloads [][]byte, source string) ([]ObjectSpec, error) {
	var specs []ObjectSpec
	for idx, payload := range payloads {
		parsed, err := parseObjectSpecsJSON(payload, fmt.Sprintf("%s[%d]", source, idx))
		if err != nil {
			return nil, err
		}
		specs = append(specs, parsed...)
	}
	return specs, nil
}

func sourceBackedAutoNumberFormatSeedsFromResolvedSource(source *ResolvedPackSource) ([]AutoNumberFormatSeed, error) {
	if source == nil || source.Manifest == nil {
		return nil, nil
	}
	var seeds []AutoNumberFormatSeed
	for _, component := range source.Manifest.Components {
		if component.Kind != ComponentKindAutoNumberFormat {
			continue
		}
		payload, err := sourceBackedComponentPayload(source, component)
		if err != nil {
			return nil, err
		}
		formats, _, err := sourceBackedAutoNumberFormatProjection(payload, componentPayloadLabel(component))
		if err != nil {
			return nil, err
		}
		seeds = append(seeds, formats...)
	}
	return seeds, nil
}

func sourceBackedAutoNumberFormatSeedsFromSourcePack(pack *SourceBackedPack) ([]AutoNumberFormatSeed, error) {
	if pack == nil {
		return nil, nil
	}
	var seeds []AutoNumberFormatSeed
	for _, component := range pack.Components() {
		if component == nil || component.Kind() != ComponentKindAutoNumberFormat {
			continue
		}
		payload, err := component.ExpectedPayload()
		if err != nil {
			return nil, err
		}
		formats, _, err := sourceBackedAutoNumberFormatProjection(payload, string(component.Kind())+":"+component.BusinessKey())
		if err != nil {
			return nil, err
		}
		seeds = append(seeds, formats...)
	}
	return seeds, nil
}

func ValidateDefaultChildRowsForSpecs(
	ctx context.Context,
	specs []ObjectSpec,
	fallback records.DefaultChildRowsSchemaSnapshot,
	fallbackEngineOwned func(string) map[string]bool,
	allowUnresolvedExternal bool,
) error {
	if len(specs) == 0 {
		return nil
	}
	snapshot := NewObjectMetadataSnapshot(specs, fallback)
	engineOwned := DefaultChildRowsEngineOwnedByObject(specs, fallbackEngineOwned)
	for _, obj := range buildObjectMetadata(specs) {
		if obj == nil || len(obj.DefaultChildRows) == 0 {
			continue
		}
		rows := obj.DefaultChildRows
		if allowUnresolvedExternal && fallback == nil {
			filtered := make([]models.DefaultChildRow, 0, len(rows))
			for _, row := range rows {
				targetName := strings.TrimSpace(row.TargetObject)
				if targetName == "" || snapshot.GetSchema(ctx, targetName) != nil {
					filtered = append(filtered, row)
				}
			}
			rows = filtered
		}
		if len(rows) == 0 {
			continue
		}
		if _, err := records.ResolveDefaultChildRowsContracts(ctx, snapshot, obj, rows, engineOwned); err != nil {
			return err
		}
	}
	return nil
}
