package programchecker

import (
	"fmt"
	"strings"

	"github.com/nexuscrm/kernel/pkg/flowactions"
	"github.com/nexuscrm/kernel/pkg/formula/spec"
	"github.com/nexuscrm/shared/pkg/constants"
	sharedcontract "github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	connectorcontract "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
)

func (c *flowChecker) checkActionCalls() {
	for i, action := range c.meta.ActionCalls {
		c.withElementScope(action.Name, func() {
			base := fmt.Sprintf("/actionCalls/%d", i)
			c.checkActionOutputTargetReferences(action, base)
			c.checkActionSignature(action, base)
			c.checkActionConnectorMappings(action, base)
		})
	}
}

func (c *flowChecker) checkActionSignature(action models.FlowActionCall, pointerBase string) {
	if c.opts.Actions == nil {
		return
	}
	signatureKey, sig, ok, err := c.resolveActionSignature(action)
	if err != nil {
		c.readerError(pointerBase+"/actionName", action.Name, err)
		return
	}
	if strings.TrimSpace(action.ActionName) == "" && strings.TrimSpace(action.ActionType) == "" {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_action",
			Severity:    SeverityError,
			JSONPointer: pointerBase + "/actionName",
			ElementName: action.Name,
			Message:     "actionName and actionType are blank",
		})
		return
	}
	if !ok {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_action",
			Severity:    SeverityError,
			JSONPointer: pointerBase + "/actionName",
			ElementName: action.Name,
			Reference:   signatureKey,
			Message:     fmt.Sprintf("action signature %q does not exist", signatureKey),
		})
		return
	}
	if sig.SkipParameterValidation {
		return
	}
	c.checkActionInputs(action, sig, pointerBase, signatureKey)
	c.checkActionOutputs(action, sig, pointerBase, signatureKey)
}

func (c *flowChecker) resolveActionSignature(action models.FlowActionCall) (string, ActionSignature, bool, error) {
	keys := actionSignatureLookupKeys(action)
	for _, key := range keys {
		sig, ok, err := c.opts.Actions.ActionSignature(c.ctx, key)
		if err != nil || ok {
			return key, sig, ok, err
		}
	}
	if len(keys) == 0 {
		return "", ActionSignature{}, false, nil
	}
	return keys[0], ActionSignature{}, false, nil
}

func actionSignatureLookupKeys(action models.FlowActionCall) []string {
	var out []string
	add := func(key string) {
		key = strings.TrimSpace(key)
		if key == "" {
			return
		}
		for _, existing := range out {
			if strings.EqualFold(existing, key) {
				return
			}
		}
		out = append(out, key)
	}
	add(action.ActionName)
	add(action.ActionType)
	return out
}

func (c *flowChecker) checkActionInputs(action models.FlowActionCall, sig ActionSignature, pointerBase, signatureKey string) {
	inputs := actionSchemaFieldsByName(sig.Inputs)
	provided := map[string]struct{}{}
	for i, param := range action.InputParameters {
		name := strings.TrimSpace(param.Name)
		if name == "" {
			continue
		}
		key := actionSchemaFieldKey(name)
		field, ok := inputs[key]
		if !ok {
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.unknown_action_input",
				Severity:    SeverityError,
				JSONPointer: fmt.Sprintf("%s/inputParameters/%d/name", pointerBase, i),
				ElementName: action.Name,
				Reference:   signatureKey + "." + name,
				Message:     fmt.Sprintf("action signature %q has no input parameter %q", signatureKey, name),
			})
			continue
		}
		if actionInputValuePresent(param.Value) {
			provided[key] = struct{}{}
		}
		actual, ok := c.typeRefFromValue(param.Value, fmt.Sprintf("%s/inputParameters/%d/value", pointerBase, i), action.Name)
		if !ok {
			continue
		}
		c.checkTypeCompatible(typeRefFromActionSchemaField(field, ValueSourceActionInput), actual, fmt.Sprintf("%s/inputParameters/%d/value", pointerBase, i), action.Name, signatureKey+"."+name)
	}
	for _, field := range sig.Inputs {
		if !actionInputRequired(field) {
			continue
		}
		if _, ok := provided[actionSchemaFieldKey(field.Name)]; ok {
			continue
		}
		c.addDiagnostic(Diagnostic{
			Code:         "metadata_program.missing_action_input",
			Severity:     SeverityError,
			JSONPointer:  pointerBase + "/inputParameters",
			ElementName:  action.Name,
			Reference:    signatureKey + "." + field.Name,
			ExpectedType: typeRefFromActionSchemaField(field, ValueSourceActionInput).String(),
			Message:      fmt.Sprintf("action signature %q is missing required input parameter %q", signatureKey, field.Name),
		})
	}
}

func (c *flowChecker) checkActionOutputs(action models.FlowActionCall, sig ActionSignature, pointerBase, signatureKey string) {
	outputs := actionSchemaFieldsByName(sig.Outputs)
	for i, param := range action.OutputParameters {
		name := strings.TrimSpace(param.Name)
		if name == "" {
			continue
		}
		field, ok := outputs[actionSchemaFieldKey(name)]
		if !ok {
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.unknown_action_output",
				Severity:    SeverityError,
				JSONPointer: fmt.Sprintf("%s/outputParameters/%d/name", pointerBase, i),
				ElementName: action.Name,
				Reference:   signatureKey + "." + name,
				Message:     fmt.Sprintf("action signature %q has no output parameter %q", signatureKey, name),
			})
			continue
		}
		target := flowActionOutputTarget(param.Value)
		if target == "" {
			continue
		}
		if _, _, collides := c.scopedLoopIteratorRootWrite(target, action.Name); collides {
			continue
		}
		actual := typeRefFromActionSchemaField(field, ValueSourceActionOutput)
		if !strings.Contains(normalizeFlowReference(target), ".") {
			if expected, exists := c.variables[normalizeFlowReference(target)]; exists {
				c.checkTypeCompatible(expected, actual, fmt.Sprintf("%s/outputParameters/%d/value", pointerBase, i), action.Name, target)
			}
			if flowActionWritesOutputParameters(action.ActionType) {
				c.refineOutputReferenceRoot(target, actual)
			}
			continue
		}
		c.checkAssignableReference(target, actual, fmt.Sprintf("%s/outputParameters/%d/value", pointerBase, i), action.Name)
	}
}

func (c *flowChecker) checkActionOutputTargetReferences(action models.FlowActionCall, pointerBase string) {
	if strings.EqualFold(strings.TrimSpace(action.ActionType), flowactions.ActionTypePreviewRecordUpdate) {
		for i, param := range action.InputParameters {
			if !strings.EqualFold(strings.TrimSpace(param.Name), "outputReference") {
				continue
			}
			target, ok := rawFlowActionOutputTarget(param.Value)
			if !ok || strings.TrimSpace(target) == "" {
				continue
			}
			if flowReferenceWasMergeWrapped(target) {
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.invalid_reference",
					Severity:    SeverityError,
					JSONPointer: fmt.Sprintf("%s/inputParameters/%d/value", pointerBase, i),
					ElementName: action.Name,
					Reference:   target,
					Message:     fmt.Sprintf("previewRecordUpdate outputReference %q must be a runtime reference without merge syntax", target),
				})
				continue
			}
			root, ok := flowOutputReferenceRoot(target)
			if !ok {
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.invalid_reference",
					Severity:    SeverityError,
					JSONPointer: fmt.Sprintf("%s/inputParameters/%d/value", pointerBase, i),
					ElementName: action.Name,
					Reference:   target,
					Message:     fmt.Sprintf("previewRecordUpdate outputReference %q must be a single variable symbol; dotted references are not resolvable by the Flow runtime context", target),
				})
				continue
			}
			if !flowPreviewRecordUpdateOutputRootIsDeclarable(root) {
				c.addDiagnostic(Diagnostic{
					Code:        "metadata_program.invalid_reference",
					Severity:    SeverityError,
					JSONPointer: fmt.Sprintf("%s/inputParameters/%d/value", pointerBase, i),
					ElementName: action.Name,
					Reference:   target,
					Message:     fmt.Sprintf("previewRecordUpdate outputReference %q collides with a runtime-owned Flow reference root", target),
				})
				continue
			}
			c.checkScopedLoopIteratorRootWrite(target, fmt.Sprintf("%s/inputParameters/%d/value", pointerBase, i), action.Name, "previewRecordUpdate outputReference")
		}
	}
	if !flowActionWritesOutputParameters(action.ActionType) {
		return
	}
	for i, param := range action.OutputParameters {
		target, ok := rawFlowActionOutputTarget(param.Value)
		if !ok || strings.TrimSpace(target) == "" {
			continue
		}
		if flowReferenceWasMergeWrapped(target) {
			c.addDiagnostic(Diagnostic{
				Code:        "metadata_program.invalid_reference",
				Severity:    SeverityError,
				JSONPointer: fmt.Sprintf("%s/outputParameters/%d/value", pointerBase, i),
				ElementName: action.Name,
				Reference:   target,
				Message:     fmt.Sprintf("action output target %q must be a runtime reference without merge syntax", target),
			})
			continue
		}
		c.checkScopedLoopIteratorRootWrite(target, fmt.Sprintf("%s/outputParameters/%d/value", pointerBase, i), action.Name, "action output")
	}
}

func flowPreviewRecordUpdateOutputRootIsDeclarable(root string) bool {
	if strings.EqualFold(strings.TrimSpace(root), "$Record") {
		return true
	}
	return flowOutputReferenceRootIsDeclarable(root)
}

func (c *flowChecker) checkActionConnectorMappings(action models.FlowActionCall, pointerBase string) {
	if !isApplyConnectorFieldMappingsAction(action) {
		return
	}
	if c.opts.Connectors == nil {
		return
	}
	params := staticActionInputStrings(action.InputParameters)
	source := strings.TrimSpace(params["source"])
	ownerPack := strings.TrimSpace(c.opts.OwnerPack)
	if source == "" {
		source = ownerPack
	} else if ownerPack != "" && source != ownerPack {
		c.addInvalidConnectorMapping(pointerBase+"/inputParameters", action.Name, "source", fmt.Sprintf("connector mapping source assertion %q does not match owner pack %q", source, ownerPack))
		return
	}
	if source == "" {
		return
	}
	sourceObject := strings.TrimSpace(params["sourceObject"])
	targetObject := strings.TrimSpace(params["targetObject"])
	actionType := strings.TrimSpace(params["actionType"])
	direction := strings.TrimSpace(params["direction"])
	if sourceObject == "" || targetObject == "" || actionType == "" || direction == "" {
		return
	}
	if !validConnectorMappingDirection(direction) {
		c.addInvalidConnectorMapping(pointerBase+"/inputParameters", action.Name, "direction", fmt.Sprintf("connector mapping direction %q is not valid", direction))
		return
	}
	if _, ok := c.requireObject(targetObject, pointerBase+"/inputParameters", action.Name); !ok {
		return
	}
	query := connectorcontract.ResolveQuery{
		Source:       source,
		SourceObject: sourceObject,
		TargetObject: targetObject,
		ActionType:   actionType,
		Direction:    direction,
	}
	mappings, err := c.opts.Connectors.ConnectorMappings(c.ctx, query)
	if err != nil {
		c.readerError(pointerBase+"/inputParameters", action.Name, err)
		return
	}
	if len(mappings) == 0 {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_connector_mapping",
			Severity:    SeverityError,
			JSONPointer: pointerBase + "/inputParameters",
			ElementName: action.Name,
			Reference:   connectorMappingReference(query),
			Message:     fmt.Sprintf("connector mapping %s does not exist", connectorMappingReference(query)),
		})
		return
	}
	for i, row := range mappings {
		c.checkConnectorMappingRow(query, targetObject, row, fmt.Sprintf("%s/connectorMappings/%d", pointerBase, i), action.Name)
	}
}

func (c *flowChecker) checkConnectorMappingRow(query connectorcontract.ResolveQuery, targetObject string, row connectorcontract.ConnectorFieldMappingInput, pointer, elementName string) {
	if !strings.EqualFold(strings.TrimSpace(row.Source), query.Source) ||
		!strings.EqualFold(strings.TrimSpace(row.SourceObject), query.SourceObject) ||
		!strings.EqualFold(strings.TrimSpace(row.TargetObject), query.TargetObject) ||
		!strings.EqualFold(strings.TrimSpace(row.ActionType), query.ActionType) ||
		!connectorMappingDirectionMatches(query.Direction, row.Direction) {
		c.addInvalidConnectorMapping(pointer, elementName, connectorMappingReference(query), "resolved connector mapping row does not match the requested tuple")
		return
	}
	if strings.TrimSpace(row.TargetField) == "" {
		c.addInvalidConnectorMapping(pointer, elementName, connectorMappingReference(query), "connector mapping targetField is blank")
		return
	}
	field, ok := c.requireField(targetObject, row.TargetField, pointer+"/targetField", elementName)
	if !ok {
		return
	}
	if constants.NormalizeFieldType(string(field.Type)) == constants.FieldTypeAutonumber {
		c.addInvalidConnectorMapping(pointer+"/targetField", elementName, targetObject+"."+row.TargetField, "connector mapping targetField is autonumber and cannot be written")
		return
	}
	if !field.Updateable {
		c.addInvalidConnectorMapping(pointer+"/targetField", elementName, targetObject+"."+row.TargetField, "connector mapping targetField is not updateable")
	}
}

func (c *flowChecker) checkFieldMappingRef(ref *models.FlowFieldMappingRef, fallbackTargetObject, fallbackTargetReference, pointerBase, elementName string) {
	if ref == nil {
		return
	}
	sourceObject := strings.TrimSpace(ref.SourceObject)
	if sourceObject == "" {
		c.addInvalidFieldMapping(pointerBase+"/sourceObject", elementName, "sourceObject", "field mapping sourceObject is blank")
	} else if _, ok := c.requireObject(sourceObject, pointerBase+"/sourceObject", elementName); ok {
		if sourceRef := strings.TrimSpace(ref.SourceReference); sourceRef != "" {
			if actual, resolved := c.resolveReference(sourceRef, pointerBase+"/sourceReference", elementName); resolved {
				c.checkTypeCompatible(TypeRef{Scalar: spec.TypeObject, ObjectAPIName: sourceObject, Nullable: true, Source: ValueSourceConnectorMapping}, actual, pointerBase+"/sourceReference", elementName, sourceRef)
			}
		}
	}
	if strings.TrimSpace(ref.SourceReference) == "" {
		c.addInvalidFieldMapping(pointerBase+"/sourceReference", elementName, "sourceReference", "field mapping sourceReference is blank")
	}
	targetObject := strings.TrimSpace(ref.TargetObject)
	if targetObject == "" {
		targetObject = strings.TrimSpace(fallbackTargetObject)
	}
	if targetObject == "" {
		c.addInvalidFieldMapping(pointerBase+"/targetObject", elementName, "targetObject", "field mapping targetObject is blank")
	} else {
		c.requireObject(targetObject, pointerBase+"/targetObject", elementName)
	}
	if strings.TrimSpace(ref.ActionType) == "" {
		c.addInvalidFieldMapping(pointerBase+"/actionType", elementName, "actionType", "field mapping actionType is blank")
	}
	switch strings.TrimSpace(ref.MergeMode) {
	case "", "overwrite", "fillBlankOnly":
	default:
		c.addInvalidFieldMapping(pointerBase+"/mergeMode", elementName, ref.MergeMode, fmt.Sprintf("field mapping mergeMode %q is not supported", ref.MergeMode))
	}
	targetReference := strings.TrimSpace(ref.TargetReference)
	if targetReference == "" {
		targetReference = strings.TrimSpace(fallbackTargetReference)
	}
	if strings.EqualFold(strings.TrimSpace(ref.MergeMode), "fillBlankOnly") && targetReference == "" {
		c.addInvalidFieldMapping(pointerBase+"/targetReference", elementName, "targetReference", "field mapping mergeMode fillBlankOnly requires targetReference or inputReference")
	}
	if targetReference != "" && targetObject != "" {
		if actual, resolved := c.resolveReferenceOrRelationshipObject(targetReference, pointerBase+"/targetReference", elementName); resolved {
			c.checkTypeCompatible(TypeRef{Scalar: spec.TypeObject, ObjectAPIName: targetObject, Nullable: true, Source: ValueSourceConnectorMapping}, actual, pointerBase+"/targetReference", elementName, targetReference)
		}
	}
}

func actionSchemaFieldsByName(fields []sharedcontract.StandardActionSchemaField) map[string]sharedcontract.StandardActionSchemaField {
	out := make(map[string]sharedcontract.StandardActionSchemaField, len(fields))
	for _, field := range fields {
		key := actionSchemaFieldKey(field.Name)
		if key != "" {
			out[key] = field
		}
	}
	return out
}

func actionSchemaFieldKey(name string) string {
	return strings.TrimSpace(name)
}

func actionInputRequired(field sharedcontract.StandardActionSchemaField) bool {
	return field.Required && !field.Defaulted && !field.HasDefaultValue && !field.TargetDefaulted
}

func actionInputValuePresent(value any) bool {
	switch v := value.(type) {
	case nil:
		return false
	case string:
		return strings.TrimSpace(v) != ""
	case map[string]any:
		if _, ok := v["nullValue"]; ok {
			return false
		}
		if raw, ok := v["stringValue"]; ok {
			s, _ := raw.(string)
			return strings.TrimSpace(s) != ""
		}
		if raw, ok := v["elementReference"]; ok {
			s, _ := raw.(string)
			return strings.TrimSpace(s) != ""
		}
		return len(v) > 0
	default:
		return true
	}
}

func typeRefFromActionSchemaField(field sharedcontract.StandardActionSchemaField, source ValueSource) TypeRef {
	raw := strings.ToLower(strings.TrimSpace(field.Type))
	collection := field.IsCollection
	for strings.HasSuffix(raw, "[]") {
		collection = true
		raw = strings.TrimSpace(strings.TrimSuffix(raw, "[]"))
	}
	if inner, ok := bracketedActionType(raw, "array"); ok {
		collection = true
		raw = inner
	}
	if inner, ok := bracketedActionType(raw, "list"); ok {
		collection = true
		raw = inner
	}
	if inner, ok := bracketedActionType(raw, "collection"); ok {
		collection = true
		raw = inner
	}
	scalar := spec.TypeUnknown
	switch raw {
	case "string", "text", "textarea", "id", "email", "phone", "url", "picklist":
		scalar = spec.TypeString
	case "number", "double", "currency", "percent", "integer", "int", "long":
		scalar = spec.TypeNumber
	case "boolean", "bool":
		scalar = spec.TypeBoolean
	case "date":
		scalar = spec.TypeDate
	case "datetime", "date/time":
		scalar = spec.TypeDatetime
	case "object", "record":
		scalar = spec.TypeObject
	case "json", "map", "any", "":
		scalar = spec.TypeAny
	}
	return TypeRef{
		Scalar:        scalar,
		Collection:    collection,
		ObjectAPIName: actionSchemaObjectName(field),
		Nullable:      !field.Required,
		Source:        source,
	}
}

func actionSchemaObjectName(field sharedcontract.StandardActionSchemaField) string {
	if ref := strings.TrimSpace(field.ReferenceTo); ref != "" {
		return ref
	}
	return strings.TrimSpace(field.TargetObject)
}

func bracketedActionType(raw, prefix string) (string, bool) {
	open := prefix + "<"
	if strings.HasPrefix(raw, open) && strings.HasSuffix(raw, ">") {
		return strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(raw, open), ">")), true
	}
	return "", false
}

func staticActionInputStrings(params []models.FlowActionParameter) map[string]string {
	out := map[string]string{}
	for _, param := range params {
		name := actionSchemaFieldKey(param.Name)
		if name == "" {
			continue
		}
		if value, ok := staticStringValue(param.Value); ok {
			out[name] = value
		}
	}
	return out
}

func staticStringValue(value any) (string, bool) {
	switch v := value.(type) {
	case string:
		return strings.TrimSpace(v), true
	case map[string]any:
		if raw, ok := v["stringValue"]; ok {
			if s, ok := raw.(string); ok {
				return strings.TrimSpace(s), true
			}
		}
		return "", false
	default:
		return "", false
	}
}

func flowActionOutputTarget(value any) string {
	target, ok := rawFlowActionOutputTarget(value)
	if !ok || flowReferenceWasMergeWrapped(target) {
		return ""
	}
	return normalizeFlowReference(target)
}

func rawFlowActionOutputTarget(value any) (string, bool) {
	if target, ok := staticStringValue(value); ok {
		return strings.TrimSpace(target), true
	}
	if m, ok := value.(map[string]any); ok {
		if raw, ok := m["elementReference"]; ok {
			if s, ok := raw.(string); ok {
				return strings.TrimSpace(s), true
			}
		}
	}
	return "", false
}

func flowActionWritesOutputParameters(actionType string) bool {
	return strings.EqualFold(actionType, flowactions.ActionTypeFlow) ||
		strings.EqualFold(actionType, flowactions.ActionTypeSendEmail) ||
		strings.EqualFold(actionType, flowactions.ActionTypeResolveEmailRecipients) ||
		strings.EqualFold(actionType, flowactions.ActionTypeResolveConnectorRemoteIdentity)
}

func flowBuiltinActionOutputTypeForAction(action models.FlowActionCall, outputName string) (TypeRef, bool) {
	return flowBuiltinActionOutputType(action.ActionType, outputName)
}

func flowBuiltinActionOutputType(actionType, outputName string) (TypeRef, bool) {
	outputName = strings.TrimSpace(outputName)
	typ := TypeRef{Nullable: true, Source: ValueSourceActionOutput}
	switch {
	case strings.EqualFold(actionType, flowactions.ActionTypeResolveEmailRecipients):
		switch outputName {
		case "emails":
			typ.Scalar = spec.TypeString
			typ.Collection = true
			return typ, true
		case "diagnostics":
			typ.Scalar = spec.TypeObject
			typ.Collection = true
			return typ, true
		case "resolvedCount", "invalidCount":
			typ.Scalar = spec.TypeNumber
			return typ, true
		}
	case strings.EqualFold(actionType, flowactions.ActionTypeSendEmail):
		switch outputName {
		case "messageId":
			typ.Scalar = spec.TypeString
			return typ, true
		case "recipientResults":
			typ.Scalar = spec.TypeObject
			typ.Collection = true
			return typ, true
		case "sentCount":
			typ.Scalar = spec.TypeNumber
			return typ, true
		}
	case strings.EqualFold(actionType, flowactions.ActionTypeResolveConnectorRemoteIdentity):
		switch outputName {
		case "authorityState", "sourceObject", "sourceRecordId", "ownerLifecycle", "mirrorStatus":
			typ.Scalar = spec.TypeString
			return typ, true
		}
	}
	return TypeRef{}, false
}

func flowPreviewRecordUpdateOutputTarget(action models.FlowActionCall) (string, string) {
	if !strings.EqualFold(strings.TrimSpace(action.ActionType), flowactions.ActionTypePreviewRecordUpdate) {
		return "", ""
	}
	params := staticActionInputStrings(action.InputParameters)
	target := strings.TrimSpace(params["outputReference"])
	for _, param := range action.InputParameters {
		if !strings.EqualFold(strings.TrimSpace(param.Name), "outputReference") {
			continue
		}
		if parsed, ok := rawFlowActionOutputTarget(param.Value); ok {
			target = parsed
		}
		break
	}
	if flowReferenceWasMergeWrapped(target) {
		return "", strings.TrimSpace(params["object"])
	}
	if target == "" {
		target = "$Record"
	}
	return normalizeFlowReference(target), strings.TrimSpace(params["object"])
}

func isApplyConnectorFieldMappingsAction(action models.FlowActionCall) bool {
	return strings.EqualFold(strings.TrimSpace(action.ActionType), flowactions.ActionTypeApplyConnectorFieldMappings) ||
		strings.EqualFold(strings.TrimSpace(action.ActionName), flowactions.ActionTypeApplyConnectorFieldMappings)
}

func validConnectorMappingDirection(direction string) bool {
	switch strings.TrimSpace(direction) {
	case connectorcontract.DirectionInbound, connectorcontract.DirectionOutbound, connectorcontract.DirectionBidirectional:
		return true
	default:
		return false
	}
}

func connectorMappingDirectionMatches(queryDirection, rowDirection string) bool {
	rowDirection = strings.TrimSpace(rowDirection)
	return rowDirection == strings.TrimSpace(queryDirection) || rowDirection == connectorcontract.DirectionBidirectional
}

func connectorMappingReference(q connectorcontract.ResolveQuery) string {
	return fmt.Sprintf("%s:%s:%s:%s:%s", q.Source, q.SourceObject, q.TargetObject, q.ActionType, q.Direction)
}

func (c *flowChecker) resolveReferenceOrRelationshipObject(ref, pointer, elementName string) (TypeRef, bool) {
	ref = normalizeFlowReference(ref)
	if ref == "" {
		return c.resolveReference(ref, pointer, elementName)
	}
	parts := strings.Split(ref, ".")
	root := parts[0]
	switch strings.ToLower(root) {
	case "$record", "__record", "$record__old", "__record__old", "$record__prior", "__record__prior":
		if c.startObject == "" {
			return c.resolveReference(ref, pointer, elementName)
		}
		if len(parts) == 1 {
			return TypeRef{Scalar: spec.TypeObject, ObjectAPIName: c.startObject, Nullable: true, Source: ValueSourceFieldReference}, true
		}
		return c.resolveObjectPathAllowFinalRelationship(c.startObject, parts[1:], pointer, elementName, ref)
	}
	if typ, ok := c.variables[root]; ok {
		if len(parts) == 1 || typ.Scalar == spec.TypeUnknown || typ.Scalar == spec.TypeAny {
			return c.resolveReference(ref, pointer, elementName)
		}
		if typ.Scalar != spec.TypeObject || strings.TrimSpace(typ.ObjectAPIName) == "" {
			return c.resolveReference(ref, pointer, elementName)
		}
		return c.resolveObjectPathAllowFinalRelationship(typ.ObjectAPIName, parts[1:], pointer, elementName, ref)
	}
	if c.startObject != "" {
		return c.resolveObjectPathAllowFinalRelationship(c.startObject, parts, pointer, elementName, ref)
	}
	return c.resolveReference(ref, pointer, elementName)
}

func (c *flowChecker) resolveObjectPathAllowFinalRelationship(objectName string, path []string, pointer, elementName, ref string) (TypeRef, bool) {
	if len(path) == 0 {
		return TypeRef{Scalar: spec.TypeObject, ObjectAPIName: objectName, Nullable: true, Source: ValueSourceFieldReference}, true
	}
	currentObject := objectName
	for i, segment := range path {
		segment = strings.TrimSpace(segment)
		if segment == "" {
			return TypeRef{}, false
		}
		if i == len(path)-1 {
			if nextObject, ok, handled := c.tryResolveRelationshipTarget(currentObject, segment, pointer, elementName, ref); handled {
				if !ok {
					return TypeRef{}, false
				}
				return TypeRef{Scalar: spec.TypeObject, ObjectAPIName: nextObject, Nullable: true, Source: ValueSourceFieldReference}, true
			}
			field, ok := c.requireField(currentObject, segment, pointer, elementName)
			if !ok {
				return TypeRef{}, false
			}
			return typeRefFromField(field), true
		}
		nextObject, ok := c.resolveRelationshipTarget(currentObject, segment, pointer, elementName, ref)
		if !ok {
			return TypeRef{}, false
		}
		currentObject = nextObject
	}
	return TypeRef{}, false
}

func (c *flowChecker) tryResolveRelationshipTarget(objectName, relationshipName, pointer, elementName, ref string) (string, bool, bool) {
	if c.opts.Metadata == nil {
		c.readerError(pointer, elementName, ErrReaderUnavailable)
		return "", false, true
	}
	fk, ok, err := c.opts.Metadata.ForeignKeyForRelationship(c.ctx, objectName, relationshipName)
	if err != nil {
		c.readerError(pointer, elementName, err)
		return "", false, true
	}
	if !ok {
		return "", false, false
	}
	targets, ok, err := c.opts.Metadata.TargetObjectsForForeignKey(c.ctx, objectName, fk)
	if err != nil {
		c.readerError(pointer, elementName, err)
		return "", false, true
	}
	if !ok || len(targets) == 0 {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.missing_relationship",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   ref,
			Message:     fmt.Sprintf("relationship %q on object %q has no target object", relationshipName, objectName),
		})
		return "", false, true
	}
	if len(targets) > 1 {
		c.addDiagnostic(Diagnostic{
			Code:        "metadata_program.polymorphic_relationship",
			Severity:    SeverityError,
			JSONPointer: pointer,
			ElementName: elementName,
			Reference:   ref,
			Message:     fmt.Sprintf("relationship %q on object %q is polymorphic and cannot be statically resolved", relationshipName, objectName),
		})
		return "", false, true
	}
	return targets[0], true, true
}

func (c *flowChecker) addInvalidConnectorMapping(pointer, elementName, reference, message string) {
	c.addDiagnostic(Diagnostic{
		Code:        "metadata_program.invalid_connector_mapping",
		Severity:    SeverityError,
		JSONPointer: pointer,
		ElementName: elementName,
		Reference:   reference,
		Message:     message,
	})
}

func (c *flowChecker) addInvalidFieldMapping(pointer, elementName, reference, message string) {
	c.addDiagnostic(Diagnostic{
		Code:        "metadata_program.invalid_field_mapping",
		Severity:    SeverityError,
		JSONPointer: pointer,
		ElementName: elementName,
		Reference:   reference,
		Message:     message,
	})
}
