package automation

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strings"

	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	connector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
)

func connectorSyncOriginMatchesPushToConnectorContract(ctx context.Context, flowOwnerPack string, contract *connector.ConnectorPushDurableMetadataContract) bool {
	if contract == nil {
		return false
	}
	if connectorSyncOriginMatchesOwner(ctx, contract.ProfileOwnerPack) {
		return true
	}
	if connectorSyncOriginMatchesOwner(ctx, contract.ConnectorOwnerKey) {
		return true
	}
	if strings.TrimSpace(contract.ProfileOwnerPack) == "" && connectorSyncOriginMatchesOwner(ctx, flowOwnerPack) {
		return true
	}
	return false
}

func connectorPushRequiredSourceFields(mappings []connector.ConnectorFieldMappingInput, routingFields []string, formulaEngine interface {
	ConnectorTransformReferencedSourceFields(string) ([]string, error)
}) ([]string, error) {
	fields := map[string]struct{}{}
	for _, field := range routingFields {
		if field = strings.TrimSpace(field); field != "" {
			fields[field] = struct{}{}
		}
	}
	for _, mapping := range mappings {
		if field := strings.TrimSpace(mapping.SourceField); field != "" {
			fields[field] = struct{}{}
		}
		if expr := strings.TrimSpace(mapping.TransformExpression); expr != "" {
			if formulaEngine == nil {
				return nil, fmt.Errorf("PushToConnector: transform %q requires a formula engine", expr)
			}
			refs, err := formulaEngine.ConnectorTransformReferencedSourceFields(expr)
			if err != nil {
				return nil, fmt.Errorf("PushToConnector: transform %q source field analysis: %w", expr, err)
			}
			for _, field := range refs {
				if field = strings.TrimSpace(field); field != "" {
					fields[field] = struct{}{}
				}
			}
		}
	}
	out := make([]string, 0, len(fields))
	for field := range fields {
		out = append(out, field)
	}
	sort.Strings(out)
	return out, nil
}

func connectorPushNext(action *models.FlowActionCall) string {
	if action != nil && action.Connector != nil {
		return action.Connector.TargetReference
	}
	return ""
}

func ConnectorPushFieldMappingFingerprint(mappings []connector.ConnectorFieldMappingInput) string {
	data, _ := json.Marshal(mappings)
	return connectorPushSHA256Hex(data)
}

func ConnectorPushContractBodyFingerprint(contract *connector.ConnectorPushDurableMetadataContract, mappings []connector.ConnectorFieldMappingInput) string {
	if contract == nil {
		return connectorPushBodyFingerprint("", mappings, nil)
	}
	return connectorPushBodyFingerprint(contract.BodyRoot, mappings, contract.Associations)
}

func ConnectorPushCreateConflictResolutionFingerprint(policy *connector.ConnectorPushCreateConflictResolutionContract) string {
	if policy == nil {
		return ""
	}
	statusCodes := append([]int(nil), policy.StatusCodes...)
	sort.Ints(statusCodes)
	diagnosticMatch := make(map[string]string, len(policy.DiagnosticMatch))
	for path, value := range policy.DiagnosticMatch {
		path = strings.TrimSpace(path)
		if path == "" {
			continue
		}
		diagnosticMatch[path] = strings.TrimSpace(value)
	}
	payload := struct {
		StatusCodes         []int                                           `json:"statusCodes"`
		DiagnosticMatch     map[string]string                               `json:"diagnosticMatch,omitempty"`
		Identity            connector.ConnectorPushConflictIdentityContract `json:"identity"`
		Resolve             connector.ConnectorPushConflictResolverContract `json:"resolve"`
		OnResolved          string                                          `json:"onResolved"`
		NoOldSnapshotPolicy string                                          `json:"noOldSnapshotPolicy,omitempty"`
	}{
		StatusCodes:     statusCodes,
		DiagnosticMatch: diagnosticMatch,
		Identity: connector.ConnectorPushConflictIdentityContract{
			SourceField:     strings.TrimSpace(policy.Identity.SourceField),
			TargetProperty:  strings.TrimSpace(policy.Identity.TargetProperty),
			RequestBodyPath: strings.TrimSpace(policy.Identity.RequestBodyPath),
			Normalization:   strings.TrimSpace(policy.Identity.Normalization),
		},
		Resolve: connector.ConnectorPushConflictResolverContract{
			Method:            strings.ToUpper(strings.TrimSpace(policy.Resolve.Method)),
			PathTemplate:      strings.TrimSpace(policy.Resolve.PathTemplate),
			ResponseIDPath:    strings.TrimSpace(policy.Resolve.ResponseIDPath),
			RequireExactlyOne: policy.Resolve.RequireExactlyOne,
		},
		OnResolved:          strings.TrimSpace(policy.OnResolved),
		NoOldSnapshotPolicy: strings.TrimSpace(policy.NoOldSnapshotPolicy),
	}
	data, _ := json.Marshal(payload)
	return connectorPushSHA256Hex(data)
}

func connectorPushBodyFingerprint(bodyRoot string, mappings []connector.ConnectorFieldMappingInput, associations []connector.ConnectorPushAssociationContract) string {
	payload := struct {
		BodyRoot     string                                       `json:"bodyRoot"`
		Mappings     []connector.ConnectorFieldMappingInput       `json:"mappings"`
		Associations []connector.ConnectorPushAssociationContract `json:"associations,omitempty"`
	}{
		BodyRoot:     strings.TrimSpace(bodyRoot),
		Mappings:     mappings,
		Associations: connectorPushAuthoredFingerprintAssociations(associations),
	}
	data, _ := json.Marshal(payload)
	return connectorPushSHA256Hex(data)
}

func connectorPushAuthoredFingerprintAssociations(associations []connector.ConnectorPushAssociationContract) []connector.ConnectorPushAssociationContract {
	if len(associations) == 0 {
		return nil
	}
	out := make([]connector.ConnectorPushAssociationContract, len(associations))
	copy(out, associations)
	for i := range out {
		out[i].TargetPreviousActionSlotIDs = nil
		out[i].TargetProfileOwnerPack = ""
		out[i].TargetProfileName = ""
	}
	return out
}

func (e *FlowExecutor) resolvePushToConnectorMappings(ctx context.Context, ownerPack string, params pushToConnectorParams) ([]connector.ConnectorFieldMappingInput, error) {
	resolverFn := e.connectorPackMappingResolverFn
	if resolverFn == nil {
		if e.connectorMappingResolver == nil {
			return nil, fmt.Errorf("PushToConnector: pack connector mapping resolver is not wired")
		}
		resolverFn = e.connectorMappingResolver.ResolvePackConnectorMappings
	}
	mappings, err := resolverFn(ctx, connector.ResolveQuery{
		Source:       ownerPack,
		SourceObject: params.SourceObject,
		TargetObject: params.TargetObject,
		ActionType:   params.MappingActionType,
		Direction:    connector.DirectionOutbound,
	})
	if err != nil {
		return nil, fmt.Errorf("PushToConnector: resolve pack-authored outbound mappings: %w", err)
	}
	if len(mappings) == 0 {
		return nil, fmt.Errorf("PushToConnector: no outbound mappings found for %s -> %s (%s)", params.SourceObject, params.TargetObject, params.MappingActionType)
	}
	if err := validateResolvedPushToConnectorMappings(mappings); err != nil {
		return nil, err
	}
	return mappings, nil
}

// selectPushToConnectorPayloadMappings narrows a contract-owned mapping closure
// for one Flow execution. It never synthesizes mappings or permits a field that
// is not present in the installed pack metadata.
func selectPushToConnectorPayloadMappings(mappings []connector.ConnectorFieldMappingInput, payloadSourceFields []string) ([]connector.ConnectorFieldMappingInput, error) {
	if len(payloadSourceFields) == 0 {
		return mappings, nil
	}
	requested, err := validatePushToConnectorPayloadSourceFieldList(payloadSourceFields)
	if err != nil {
		return nil, err
	}
	requestedSet := make(map[string]struct{}, len(requested))
	for _, field := range requested {
		requestedSet[field] = struct{}{}
	}
	known := make(map[string]struct{}, len(mappings))
	for _, mapping := range mappings {
		field := strings.TrimSpace(mapping.SourceField)
		if field != "" {
			known[field] = struct{}{}
		}
	}
	var unknown []string
	for _, field := range requested {
		if _, ok := known[field]; !ok {
			unknown = append(unknown, field)
		}
	}
	if len(unknown) > 0 {
		sort.Strings(unknown)
		return nil, fmt.Errorf("PushToConnector: payloadSourceFields selects unmapped source field(s): %s", strings.Join(unknown, ", "))
	}
	selected := make([]connector.ConnectorFieldMappingInput, 0, len(requested))
	for _, mapping := range mappings {
		if _, ok := requestedSet[strings.TrimSpace(mapping.SourceField)]; ok {
			selected = append(selected, mapping)
		}
	}
	if len(selected) == 0 {
		return nil, fmt.Errorf("PushToConnector: payloadSourceFields selected no outbound mappings")
	}
	return selected, nil
}

func validateResolvedPushToConnectorMappings(mappings []connector.ConnectorFieldMappingInput) error {
	if len(mappings) == 0 {
		return fmt.Errorf("PushToConnector: outbound mapping closure is empty")
	}
	if err := validateConnectorFieldMappingRuntimeTargetWrites(mappings); err != nil {
		return err
	}
	if needsTargetRecord, err := connectorMappingsNeedAuthoritativeTargetRecord(mappings); err != nil {
		return err
	} else if needsTargetRecord {
		return fmt.Errorf("PushToConnector: outbound PATCH v1a does not support ConflictPolicy values that require authoritative remote target state")
	}
	return nil
}

func (e *FlowExecutor) authorizePushToConnectorSourceFieldsWithEngine(
	ctx context.Context,
	sourceObject string,
	mappings []connector.ConnectorFieldMappingInput,
	routingFields []string,
	user *models.UserSession,
	formulaEngine interface {
		ConnectorTransformReferencedSourceFields(string) ([]string, error)
	},
) error {
	readCtx := pushToConnectorInvokingUserReadContext(ctx)
	fields := map[string]struct{}{}
	for _, field := range routingFields {
		if field = strings.TrimSpace(field); field != "" {
			fields[field] = struct{}{}
		}
	}
	for _, mapping := range mappings {
		if field := strings.TrimSpace(mapping.SourceField); field != "" {
			fields[field] = struct{}{}
		}
		if expr := strings.TrimSpace(mapping.TransformExpression); expr != "" {
			if formulaEngine == nil {
				return fmt.Errorf("PushToConnector: transform %q requires a formula engine", expr)
			}
			refs, err := formulaEngine.ConnectorTransformReferencedSourceFields(expr)
			if err != nil {
				return fmt.Errorf("PushToConnector: transform %q source field analysis: %w", expr, err)
			}
			for _, field := range refs {
				if field = strings.TrimSpace(field); field != "" {
					fields[field] = struct{}{}
				}
			}
		}
	}
	for field := range fields {
		visible, err := e.fieldReadAuthorizer.CheckFieldVisibilityWithUser(readCtx, sourceObject, field, user)
		if err != nil {
			return err
		}
		if !visible {
			return fmt.Errorf("PushToConnector: source field %s.%s is not readable by the invoking user", sourceObject, field)
		}
	}
	return nil
}

func (e *FlowExecutor) authorizePushToConnectorSourceRecord(ctx context.Context, sourceObject string, sourceRecord models.SObject, user *models.UserSession) error {
	readCtx := pushToConnectorInvokingUserReadContext(ctx)
	recordID, _ := sourceRecord[constants.FieldID].(string)
	if strings.TrimSpace(recordID) == "" {
		return fmt.Errorf("PushToConnector: source record Id is required for invoking-user record read authorization")
	}
	if !e.recordReadAuthorizer.CheckRecordReadAccessWithUser(readCtx, sourceObject, sourceRecord, user) {
		return fmt.Errorf("PushToConnector: source record %s/%s is not readable by the invoking user", sourceObject, recordID)
	}
	return nil
}

func pushToConnectorInvokingUserReadContext(ctx context.Context) context.Context {
	return permissions.WithAccessInputCache(constants.WithSecurityContext(ctx, constants.UserContext, "PushToConnector invoking-user read authorization"))
}

func trimmedString(value interface{}) string {
	switch v := value.(type) {
	case string:
		return strings.TrimSpace(v)
	default:
		return ""
	}
}
