package catalogsnapshot

import (
	"encoding/hex"
	"fmt"
	"sort"
	"strconv"
	"strings"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/fieldcapabilities"
	"github.com/nexuscrm/shared/pkg/models"
	catalogassignmentidentityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalogassignmentidentityv1"
	layoutvalidationviewv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutvalidationviewv1"
	outboundintentv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/outboundintentv1"
)

const maxOutboundIntentBindings = 64

func canonicalOutboundIntentRoutes(
	revision CatalogRevision,
	values []OutboundIntentRouteInput,
) ([]OutboundIntentRoute, error) {
	if len(values) == 0 {
		return nil, nil
	}
	routes := make([]OutboundIntentRoute, len(values))
	for i := range values {
		if err := requireSourceRevision(
			fmt.Sprintf("outboundIntentRoutes[%d]", i), revision, values[i].CatalogRevision,
		); err != nil {
			return nil, err
		}
		cloned, err := cloneJSON(values[i].Route)
		if err != nil {
			return nil, conflict(ConflictEncoding, fmt.Sprintf("outboundIntentRoutes[%d]", i), "clone route: %v", err)
		}
		if cloned.Operation == outboundintentv1.OperationUpsert {
			// Preserve the published v15 UPSERT wire grammar. The empty operation
			// is its canonical representation and normalizes to UPSERT at use sites.
			cloned.Operation = ""
		}
		sort.Slice(cloned.Bindings, func(left, right int) bool {
			return outboundIntentBindingKey(cloned.Bindings[left]) < outboundIntentBindingKey(cloned.Bindings[right])
		})
		routes[i] = cloned
	}
	sort.Slice(routes, func(left, right int) bool {
		return outboundIntentRouteKey(routes[left]) < outboundIntentRouteKey(routes[right])
	})
	if err := validateOutboundIntentRoutes(routes, nil); err != nil {
		return nil, err
	}
	return routes, nil
}

func validateOutboundIntentRoutes(
	routes []OutboundIntentRoute,
	mutation *mutationDefinitionDocument,
) error {
	seenRoutes := make(map[string]struct{}, len(routes))
	for i := range routes {
		route := routes[i]
		path := fmt.Sprintf("outboundIntentRoutes[%d]", i)
		for name, value := range map[string]string{
			"authorizedWorkloadId": route.AuthorizedWorkloadID,
			"connectorOwnerKey":    route.ConnectorOwnerKey,
			"flowDeveloperName":    route.FlowDeveloperName,
			"flowActionName":       route.FlowActionName,
			"sourceObject":         route.SourceObject,
			"actionSlotId":         route.ActionSlotID,
			"destinationSlotId":    route.DestinationSlotID,
		} {
			if err := requireCanonicalIdentity(path+"."+name, value); err != nil {
				return err
			}
		}
		key := outboundIntentRouteKey(route)
		if _, duplicate := seenRoutes[key]; duplicate {
			return conflict(ConflictInvalidDefinition, path, "outbound intent route is duplicated")
		}
		seenRoutes[key] = struct{}{}
		if i > 0 && outboundIntentRouteKey(routes[i-1]) >= key {
			return conflict(ConflictInvalidDefinition, path, "outbound intent routes are not canonical")
		}
		if len(route.Bindings) == 0 || len(route.Bindings) > maxOutboundIntentBindings {
			return conflict(ConflictInvalidDefinition, path+".bindings", "binding reference count is invalid")
		}
		seenBindings := make(map[string]struct{}, len(route.Bindings))
		for bindingIndex := range route.Bindings {
			binding := route.Bindings[bindingIndex]
			bindingPath := fmt.Sprintf("%s.bindings[%d]", path, bindingIndex)
			for name, value := range map[string]string{
				"ownerPack":          binding.OwnerPack,
				"packInstallationId": binding.PackInstallationID,
				"businessKey":        binding.BusinessKey,
			} {
				if err := requireCanonicalIdentity(bindingPath+"."+name, value); err != nil {
					return err
				}
			}
			if !binding.Kind.Valid() {
				return conflict(ConflictInvalidDefinition, bindingPath+".kind", "binding kind is invalid")
			}
			if !validPackPayloadDigest(binding.PackPayloadDigest) {
				return conflict(ConflictInvalidDefinition, bindingPath+".packPayloadDigest", "pack payload digest is invalid")
			}
			if binding.DefinitionVersion == 0 {
				return conflict(ConflictInvalidDefinition, bindingPath+".definitionVersion", "definition version is invalid")
			}
			bindingKey := outboundIntentBindingKey(binding)
			if _, duplicate := seenBindings[bindingKey]; duplicate ||
				(bindingIndex > 0 && outboundIntentBindingKey(route.Bindings[bindingIndex-1]) >= bindingKey) {
				return conflict(ConflictInvalidDefinition, bindingPath, "binding references are duplicated or not canonical")
			}
			seenBindings[bindingKey] = struct{}{}
		}
		switch normalizedOutboundIntentOperation(route.Operation) {
		case outboundintentv1.OperationUpsert:
			if route.MailCompletion != nil {
				return conflict(ConflictInvalidDefinition, path+".mailCompletion", "UPSERT route cannot carry mail completion authority")
			}
			if mutation != nil && !outboundIntentRouteHasFinalizationAuthority(route, mutation) {
				return conflict(ConflictInvalidDefinition, path, "route has no exact connector execution authority")
			}
		case outboundintentv1.OperationSendMail:
			if err := validateOutboundMailCompletionProjection(path+".mailCompletion", route.MailCompletion); err != nil {
				return err
			}
			if len(route.Bindings) != 1 || route.Bindings[0].Kind != "CONNECTOR_CALLOUT_PROFILE" {
				return conflict(ConflictInvalidDefinition, path+".bindings", "SEND_MAIL route requires exactly one callout-profile binding")
			}
		default:
			return conflict(ConflictInvalidDefinition, path+".operation", "outbound intent operation is invalid")
		}
	}
	return nil
}

func normalizedOutboundIntentOperation(value outboundintentv1.Operation) outboundintentv1.Operation {
	if value == "" {
		return outboundintentv1.OperationUpsert
	}
	return value
}

func outboundIntentRoutesUseMailAuthority(routes []OutboundIntentRoute) bool {
	for _, route := range routes {
		if normalizedOutboundIntentOperation(route.Operation) == outboundintentv1.OperationSendMail ||
			route.MailCompletion != nil {
			return true
		}
	}
	return false
}

func outboundIntentRoutesUseMailV2Authority(routes []OutboundIntentRoute) bool {
	for _, route := range routes {
		if normalizedOutboundIntentOperation(route.Operation) != outboundintentv1.OperationSendMail || route.MailCompletion == nil {
			continue
		}
		message := route.MailCompletion.Message
		if message.CcAddressField != "" || message.BccAddressField != "" || message.HTMLBodyField != "" {
			return true
		}
	}
	return false
}

func outboundIntentRoutesUseTypedOperationGrammar(routes []OutboundIntentRoute) bool {
	for _, route := range routes {
		if route.Operation != "" || route.MailCompletion != nil {
			return true
		}
	}
	return false
}

// ValidateOutboundMailCompletionProjection validates the intrinsic,
// provider-neutral completion grammar before a caller starts durable Catalog
// lifecycle work. Object and field compatibility remains owned by snapshot
// compilation against the effective metadata closure.
func ValidateOutboundMailCompletionProjection(value *OutboundMailCompletionProjection) error {
	return validateOutboundMailCompletionProjection("outboundMailCompletion", value)
}

func validateOutboundMailCompletionProjection(path string, value *OutboundMailCompletionProjection) error {
	if value == nil {
		return conflict(ConflictInvalidDefinition, path, "SEND_MAIL route requires neutral completion authority")
	}
	activityObject, err := catalogassignmentidentityv1.NormalizeObjectAPIName(value.Activity.ObjectAPIName)
	if err != nil {
		return conflict(ConflictInvalidIdentity, path+".activity.objectApiName", "mail projection object identity: %v", err)
	}
	messageObject, err := catalogassignmentidentityv1.NormalizeObjectAPIName(value.Message.ObjectAPIName)
	if err != nil {
		return conflict(ConflictInvalidIdentity, path+".message.objectApiName", "mail projection object identity: %v", err)
	}
	mailV2FieldCount := 0
	for _, field := range []string{value.Message.CcAddressField, value.Message.BccAddressField, value.Message.HTMLBodyField} {
		if field != "" {
			mailV2FieldCount++
		}
	}
	if mailV2FieldCount != 0 && mailV2FieldCount != 3 {
		return conflict(ConflictInvalidDefinition, path+".message", "Cc, Bcc, and HTML completion fields must be declared together")
	}
	if value.Message.AttachmentIDsField != "" && mailV2FieldCount != 3 {
		return conflict(ConflictInvalidDefinition, path+".message.attachmentIdsField", "attachment completion requires Cc, Bcc, and HTML completion fields")
	}
	fields := map[string]string{
		"recipientAddressField":      value.RecipientAddressField,
		"recipientOptOutField":       value.RecipientOptOutField,
		"activity.sourceRecordField": value.Activity.SourceRecordField,
		"activity.subjectField":      value.Activity.SubjectField,
		"activity.activityDateField": value.Activity.ActivityDateField,
		"activity.statusField":       value.Activity.StatusField,
		"activity.priorityField":     value.Activity.PriorityField,
		"activity.bodyField":         value.Activity.BodyField,
		"activity.ownerField":        value.Activity.OwnerField,
		"activity.typeField":         value.Activity.TypeField,
		"activity.subtypeField":      value.Activity.SubtypeField,
		"message.activityIdField":    value.Message.ActivityIDField,
		"message.fromAddressField":   value.Message.FromAddressField,
		"message.toAddressField":     value.Message.ToAddressField,
		"message.subjectField":       value.Message.SubjectField,
		"message.textBodyField":      value.Message.TextBodyField,
		"message.incomingField":      value.Message.IncomingField,
		"message.statusField":        value.Message.StatusField,
		"message.messageDateField":   value.Message.MessageDateField,
	}
	if mailV2FieldCount == 3 {
		fields["message.ccAddressField"] = value.Message.CcAddressField
		fields["message.bccAddressField"] = value.Message.BccAddressField
		fields["message.htmlBodyField"] = value.Message.HTMLBodyField
	}
	if value.Message.AttachmentIDsField != "" {
		fields["message.attachmentIdsField"] = value.Message.AttachmentIDsField
	}
	for name, field := range fields {
		if _, err := layoutvalidationviewv1.NormalizeFieldAPIName(field); err != nil {
			return conflict(ConflictInvalidIdentity, path+"."+name, "mail projection field identity: %v", err)
		}
	}
	if err := validateOutboundMailCompletionRecordFields(path+".activity", []string{
		constants.FieldID,
		value.Activity.SourceRecordField,
		value.Activity.SubjectField,
		value.Activity.ActivityDateField,
		value.Activity.StatusField,
		value.Activity.PriorityField,
		value.Activity.BodyField,
		value.Activity.OwnerField,
		value.Activity.TypeField,
		value.Activity.SubtypeField,
	}); err != nil {
		return err
	}
	messageFields := []string{
		constants.FieldID,
		value.Message.ActivityIDField,
		value.Message.FromAddressField,
		value.Message.ToAddressField,
		value.Message.SubjectField,
		value.Message.TextBodyField,
		value.Message.IncomingField,
		value.Message.StatusField,
		value.Message.MessageDateField,
	}
	if mailV2FieldCount == 3 {
		messageFields = append(messageFields,
			value.Message.CcAddressField, value.Message.BccAddressField, value.Message.HTMLBodyField,
		)
	}
	if value.Message.AttachmentIDsField != "" {
		messageFields = append(messageFields, value.Message.AttachmentIDsField)
	}
	if err := validateOutboundMailCompletionRecordFields(path+".message", messageFields); err != nil {
		return err
	}
	for name, fixed := range map[string]string{
		"activity.fixedSubject":  value.Activity.FixedSubject,
		"activity.fixedStatus":   value.Activity.FixedStatus,
		"activity.fixedPriority": value.Activity.FixedPriority,
		"activity.fixedType":     value.Activity.FixedType,
		"activity.fixedSubtype":  value.Activity.FixedSubtype,
		"message.fixedStatus":    value.Message.FixedStatus,
	} {
		if fixed == "" || strings.TrimSpace(fixed) != fixed || len(fixed) > 255 {
			return conflict(ConflictInvalidDefinition, path+"."+name, "mail projection fixed value is invalid")
		}
	}
	if value.Message.FixedIncoming {
		return conflict(ConflictInvalidDefinition, path+".message.fixedIncoming", "SEND_MAIL message completion cannot be incoming")
	}
	if activityObject == messageObject {
		return conflict(ConflictInvalidDefinition, path, "mail activity and message objects must be distinct")
	}
	return nil
}

func validateOutboundMailCompletionRecordFields(path string, fields []string) error {
	seen := make(map[string]string, len(fields))
	for _, field := range fields {
		key := foldIdentity(field)
		if previous, duplicate := seen[key]; duplicate {
			return conflict(
				ConflictDuplicateField,
				path,
				"mail completion fields %q and %q alias the same record field",
				previous,
				field,
			)
		}
		seen[key] = field
	}
	return nil
}

func validateOutboundMailCompletionMetadata(
	routes []OutboundIntentRoute,
	objects map[string]*models.ObjectMetadata,
) error {
	for routeIndex, route := range routes {
		if normalizedOutboundIntentOperation(route.Operation) != outboundintentv1.OperationSendMail {
			continue
		}
		completion := route.MailCompletion
		if completion == nil {
			return fmt.Errorf("outboundIntentRoutes[%d].mailCompletion is absent", routeIndex)
		}
		path := fmt.Sprintf("outboundIntentRoutes[%d].mailCompletion", routeIndex)
		requirements := []outboundMailFieldRequirement{
			{path + ".recipientAddressField", route.SourceObject, completion.RecipientAddressField, "", "", false, false, []constants.SchemaFieldType{constants.FieldTypeEmail, constants.FieldTypeString}},
			{path + ".recipientOptOutField", route.SourceObject, completion.RecipientOptOutField, "", "", false, false, []constants.SchemaFieldType{constants.FieldTypeBoolean}},
			{path + ".activity.sourceRecordField", completion.Activity.ObjectAPIName, completion.Activity.SourceRecordField, route.SourceObject, "", true, false, []constants.SchemaFieldType{constants.FieldTypeReference, constants.FieldTypeMasterDetail}},
			{path + ".activity.subjectField", completion.Activity.ObjectAPIName, completion.Activity.SubjectField, "", completion.Activity.FixedSubject, true, false, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypePicklist}},
			{path + ".activity.activityDateField", completion.Activity.ObjectAPIName, completion.Activity.ActivityDateField, "", "", true, false, []constants.SchemaFieldType{constants.FieldTypeDate}},
			{path + ".activity.statusField", completion.Activity.ObjectAPIName, completion.Activity.StatusField, "", completion.Activity.FixedStatus, true, false, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypePicklist}},
			{path + ".activity.priorityField", completion.Activity.ObjectAPIName, completion.Activity.PriorityField, "", completion.Activity.FixedPriority, true, false, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypePicklist}},
			{path + ".activity.bodyField", completion.Activity.ObjectAPIName, completion.Activity.BodyField, "", "", true, false, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypeTextarea, constants.FieldTypeRichtextarea}},
			{path + ".activity.ownerField", completion.Activity.ObjectAPIName, completion.Activity.OwnerField, "", "", true, false, []constants.SchemaFieldType{constants.FieldTypeReference, constants.FieldTypeMasterDetail}},
			{path + ".activity.typeField", completion.Activity.ObjectAPIName, completion.Activity.TypeField, "", completion.Activity.FixedType, true, false, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypePicklist}},
			{path + ".activity.subtypeField", completion.Activity.ObjectAPIName, completion.Activity.SubtypeField, "", completion.Activity.FixedSubtype, true, false, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypePicklist}},
			{path + ".message.activityIdField", completion.Message.ObjectAPIName, completion.Message.ActivityIDField, completion.Activity.ObjectAPIName, "", true, false, []constants.SchemaFieldType{constants.FieldTypeReference, constants.FieldTypeMasterDetail}},
			{path + ".message.fromAddressField", completion.Message.ObjectAPIName, completion.Message.FromAddressField, "", "", true, false, []constants.SchemaFieldType{constants.FieldTypeEmail, constants.FieldTypeString}},
			{path + ".message.toAddressField", completion.Message.ObjectAPIName, completion.Message.ToAddressField, "", "", true, false, []constants.SchemaFieldType{constants.FieldTypeEmail, constants.FieldTypeString}},
			{path + ".message.subjectField", completion.Message.ObjectAPIName, completion.Message.SubjectField, "", "", true, false, []constants.SchemaFieldType{constants.FieldTypeString}},
			{path + ".message.textBodyField", completion.Message.ObjectAPIName, completion.Message.TextBodyField, "", "", true, false, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypeTextarea, constants.FieldTypeRichtextarea}},
			{path + ".message.incomingField", completion.Message.ObjectAPIName, completion.Message.IncomingField, "", "", true, false, []constants.SchemaFieldType{constants.FieldTypeBoolean}},
			{path + ".message.statusField", completion.Message.ObjectAPIName, completion.Message.StatusField, "", completion.Message.FixedStatus, true, false, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypePicklist}},
			{path + ".message.messageDateField", completion.Message.ObjectAPIName, completion.Message.MessageDateField, "", "", true, false, []constants.SchemaFieldType{constants.FieldTypeDatetime}},
		}
		if completion.Message.CcAddressField != "" {
			requirements = append(requirements,
				outboundMailFieldRequirement{path + ".message.ccAddressField", completion.Message.ObjectAPIName, completion.Message.CcAddressField, "", "", true, true, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypeTextarea}},
				outboundMailFieldRequirement{path + ".message.bccAddressField", completion.Message.ObjectAPIName, completion.Message.BccAddressField, "", "", true, true, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypeTextarea}},
				outboundMailFieldRequirement{path + ".message.htmlBodyField", completion.Message.ObjectAPIName, completion.Message.HTMLBodyField, "", "", true, true, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypeTextarea, constants.FieldTypeRichtextarea}},
			)
		}
		if completion.Message.AttachmentIDsField != "" {
			requirements = append(requirements,
				outboundMailFieldRequirement{path + ".message.attachmentIdsField", completion.Message.ObjectAPIName, completion.Message.AttachmentIDsField, "", "", true, true, []constants.SchemaFieldType{constants.FieldTypeString, constants.FieldTypeTextarea}},
			)
		}
		for _, requirement := range requirements {
			object := objects[foldIdentity(requirement.object)]
			if object == nil {
				return fmt.Errorf("%s references absent object %q", requirement.path, requirement.object)
			}
			field := models.FindField(object, requirement.field)
			if field == nil {
				return fmt.Errorf("%s references absent field %q.%s", requirement.path, requirement.object, requirement.field)
			}
			fieldType := constants.NormalizeFieldType(string(field.Type))
			if !outboundMailFieldTypeAllowed(fieldType, requirement.types) {
				return fmt.Errorf("%s field %q.%s has incompatible type %q", requirement.path, requirement.object, requirement.field, fieldType)
			}
			if requirement.createable {
				createable, _ := fieldcapabilities.ApplyWriteProtectionPolicy(field.Createable, field.Updateable, field.WriteProtectionPolicy)
				if !createable {
					return fmt.Errorf("%s field %q.%s is not createable", requirement.path, requirement.object, requirement.field)
				}
			}
			if requirement.mustAllowBlank && field.Required {
				return fmt.Errorf("%s field %q.%s cannot be required", requirement.path, requirement.object, requirement.field)
			}
			if requirement.referenceTarget != "" && !outboundMailReferenceFieldAccepts(field, requirement.referenceTarget) {
				return fmt.Errorf("%s field %q.%s cannot reference %q", requirement.path, requirement.object, requirement.field, requirement.referenceTarget)
			}
			if requirement.fixedValue != "" && fieldType == constants.FieldTypePicklist && !outboundMailPicklistContains(field.Options, requirement.fixedValue) {
				return fmt.Errorf("%s field %q.%s has no fixed value %q", requirement.path, requirement.object, requirement.field, requirement.fixedValue)
			}
		}
	}
	return nil
}

type outboundMailFieldRequirement struct {
	path, object, field, referenceTarget, fixedValue string
	createable                                       bool
	mustAllowBlank                                   bool
	types                                            []constants.SchemaFieldType
}

func outboundMailFieldTypeAllowed(value constants.SchemaFieldType, allowed []constants.SchemaFieldType) bool {
	for _, candidate := range allowed {
		if value == candidate {
			return true
		}
	}
	return false
}

func outboundMailReferenceFieldAccepts(field *models.FieldMetadata, target string) bool {
	for _, candidate := range field.ReferenceTo {
		if strings.EqualFold(candidate, target) {
			return true
		}
	}
	return false
}

func outboundMailPicklistContains(options models.PicklistOptions, value string) bool {
	for _, option := range options {
		if option.Value == value {
			return true
		}
	}
	return false
}

func outboundIntentRouteHasFinalizationAuthority(
	route OutboundIntentRoute,
	mutation *mutationDefinitionDocument,
) bool {
	if mutation == nil {
		return false
	}
	for _, authority := range mutation.ConnectorInboundFinalization {
		contract := authority.Contract
		if contract.ConnectorOwnerKey == route.ConnectorOwnerKey &&
			contract.FlowDeveloperName == route.FlowDeveloperName &&
			contract.FlowActionName == route.FlowActionName &&
			contract.SourceObject == route.SourceObject &&
			contract.ActionSlotID == route.ActionSlotID &&
			contract.DestinationSlotID == route.DestinationSlotID {
			for _, association := range contract.Associations {
				if association.Junction != nil {
					return false
				}
			}
			return true
		}
	}
	return false
}

func outboundIntentRouteKey(route OutboundIntentRoute) string {
	parts := []string{
		string(normalizedOutboundIntentOperation(route.Operation)),
		strings.ToLower(route.ConnectorOwnerKey),
		strings.ToLower(route.FlowDeveloperName),
		strings.ToLower(route.FlowActionName),
	}
	if normalizedOutboundIntentOperation(route.Operation) == outboundintentv1.OperationSendMail {
		parts = append(parts, strings.ToLower(route.SourceObject))
	}
	return strings.Join(parts, "\x00")
}

func outboundIntentBindingKey(binding IntegrationBindingReference) string {
	return strings.Join([]string{
		string(binding.Kind), binding.OwnerPack, binding.PackInstallationID,
		binding.PackPayloadDigest, binding.BusinessKey, strconv.FormatUint(binding.DefinitionVersion, 10),
	}, "\x00")
}

func validPackPayloadDigest(value string) bool {
	const prefix = "nexus-pack-sha256-v1:"
	if len(value) != len(prefix)+64 || !strings.HasPrefix(value, prefix) || value != strings.ToLower(value) {
		return false
	}
	_, err := hex.DecodeString(strings.TrimPrefix(value, prefix))
	return err == nil
}
