package automation

import (
	"context"
	"fmt"
	"strings"

	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
)

// FlowRecordCreateFieldPreviewTarget identifies a field on a Flow recordCreate
// element whose resolved value should be returned without performing DML.
type FlowRecordCreateFieldPreviewTarget struct {
	ObjectAPIName string
	FieldAPIName  string
}

// FlowRecordCreatePreview contains resolved field values for one recordCreate
// element reached during preview execution.
type FlowRecordCreatePreview struct {
	ElementName   string                 `json:"elementName"`
	ObjectAPIName string                 `json:"objectApiName"`
	Fields        map[string]interface{} `json:"fields"`
}

// FlowRecordCreateFieldsPreviewResult is the read-only projection returned by
// Flow preview execution.
type FlowRecordCreateFieldsPreviewResult struct {
	RecordCreates []FlowRecordCreatePreview `json:"recordCreates"`
}

type recordCreatePreviewTargetSet struct {
	fieldsByObject map[string]map[string]struct{}
	seen           map[string]struct{}
	total          int
}

func newRecordCreatePreviewTargetSet(targets []FlowRecordCreateFieldPreviewTarget) (*recordCreatePreviewTargetSet, error) {
	set := &recordCreatePreviewTargetSet{
		fieldsByObject: map[string]map[string]struct{}{},
		seen:           map[string]struct{}{},
	}
	for _, target := range targets {
		objectAPIName := strings.TrimSpace(target.ObjectAPIName)
		fieldAPIName := strings.TrimSpace(target.FieldAPIName)
		if objectAPIName == "" || fieldAPIName == "" {
			return nil, apperrors.NewValidationError("previewTargets", "recordCreate preview targets require objectApiName and fieldApiName")
		}
		if set.fieldsByObject[objectAPIName] == nil {
			set.fieldsByObject[objectAPIName] = map[string]struct{}{}
		}
		key := objectAPIName + "." + fieldAPIName
		if _, exists := set.fieldsByObject[objectAPIName][fieldAPIName]; exists {
			continue
		}
		set.fieldsByObject[objectAPIName][fieldAPIName] = struct{}{}
		set.total++
		set.seen[key] = struct{}{}
		delete(set.seen, key)
	}
	if set.total == 0 {
		return nil, apperrors.NewValidationError("previewTargets", "at least one recordCreate preview target is required")
	}
	return set, nil
}

func (s *recordCreatePreviewTargetSet) fieldsForObject(objectAPIName string) (map[string]struct{}, bool) {
	fields, ok := s.fieldsByObject[objectAPIName]
	return fields, ok
}

func (s *recordCreatePreviewTargetSet) markSeen(objectAPIName string, fields map[string]struct{}) {
	for fieldAPIName := range fields {
		s.seen[objectAPIName+"."+fieldAPIName] = struct{}{}
	}
}

func (s *recordCreatePreviewTargetSet) done() bool {
	return len(s.seen) == s.total
}

// PreviewRecordCreateFields evaluates a Flow through the same read-side
// lookup, decision, assignment, field-mapping, and action-default machinery
// used by ExecuteFlow, but turns recordCreate into a no-DML preview point.
// It stops once all requested recordCreate field targets have been reached.
func (e *FlowExecutor) PreviewRecordCreateFields(
	ctx context.Context,
	flowAPIName string,
	inputs map[string]interface{},
	targets []FlowRecordCreateFieldPreviewTarget,
	user *models.UserSession,
) (*FlowRecordCreateFieldsPreviewResult, error) {
	targetSet, err := newRecordCreatePreviewTargetSet(targets)
	if err != nil {
		return nil, err
	}
	flowVersion, err := e.getActiveFlowVersion(ctx, flowAPIName, user)
	if err != nil {
		return nil, err
	}
	if flowVersion == nil {
		return nil, apperrors.NewNotFoundError("flow", flowAPIName)
	}
	ctx, blocked := e.authorizeProtectedStandardFlow(ctx, flowAPIName, flowVersion)
	if blocked != nil {
		if len(blocked.Errors) > 0 {
			return nil, apperrors.NewValidationError("flow", blocked.Errors[0].Message)
		}
		return nil, apperrors.NewValidationError("flow", "flow execution is not allowed")
	}

	flowMeta := flowVersion.Metadata
	if err := validateAtomicFlowForExecution(ctx, &flowMeta); err != nil {
		return nil, err
	}

	interview := &models.FlowInterview{
		Id:              "preview-" + flowAPIName,
		FlowVersionId:   flowVersion.Id,
		InterviewStatus: models.FlowInterviewStatusRunning,
		ContextData:     initializeFlowContext(&flowMeta, inputs),
		IsTransient:     true,
	}
	if !e.checkEntryConditionsCtx(ctx, &flowMeta, interview.ContextData) {
		return &FlowRecordCreateFieldsPreviewResult{}, nil
	}

	return e.previewRecordCreateFields(ctx, interview, &flowMeta, targetSet, user)
}

func (e *FlowExecutor) previewRecordCreateFields(
	ctx context.Context,
	interview *models.FlowInterview,
	flowMeta *models.FlowMetadata,
	targetSet *recordCreatePreviewTargetSet,
	user *models.UserSession,
) (*FlowRecordCreateFieldsPreviewResult, error) {
	firstElement, err := firstPreviewFlowElement(flowMeta)
	if err != nil {
		return nil, err
	}

	if interview.ContextData != nil {
		if _, present := interview.ContextData[flowContextKeyRootObject]; !present {
			if flowMeta.Start != nil && flowMeta.Start.Object != "" {
				interview.ContextData[flowContextKeyRootObject] = flowMeta.Start.Object
			} else if inferred := inferRootObjectType(interview.ContextData); inferred != "" {
				interview.ContextData[flowContextKeyRootObject] = inferred
			}
		}
	}
	e.evaluateFlowFormulas(ctx, interview, flowMeta, user)

	currentElement := firstElement
	if interview.CurrentElement != "" {
		currentElement = interview.CurrentElement
	}

	result := &FlowRecordCreateFieldsPreviewResult{}
	const maxIterations = 1000
	for i := 0; i < maxIterations && currentElement != ""; i++ {
		interview.CurrentElement = currentElement
		nextElement, err := e.previewElement(ctx, interview, flowMeta, currentElement, targetSet, result, user)
		if err != nil {
			return nil, apperrors.NewInternalError(fmt.Sprintf("error previewing element %q", currentElement), err)
		}
		if targetSet.done() {
			return result, nil
		}
		currentElement = nextElement
	}
	if !targetSet.done() {
		return nil, apperrors.NewValidationError("previewTargets", "requested recordCreate preview targets were not reached by this flow path")
	}
	return result, nil
}

func firstPreviewFlowElement(flowMeta *models.FlowMetadata) (string, error) {
	if flowMeta.Start != nil && flowMeta.Start.Connector != nil {
		return flowMeta.Start.Connector.TargetReference, nil
	}
	if flowMeta.StartElementReference != "" {
		return flowMeta.StartElementReference, nil
	}
	if flowMeta.Start != nil {
		for _, sp := range flowMeta.Start.ScheduledPaths {
			if sp.Connector != nil && sp.Connector.TargetReference != "" {
				return sp.Connector.TargetReference, nil
			}
		}
	}
	return "", apperrors.NewValidationError("start", "flow has no start element or startElementReference")
}

func (e *FlowExecutor) previewElement(
	ctx context.Context,
	interview *models.FlowInterview,
	flowMeta *models.FlowMetadata,
	elementName string,
	targetSet *recordCreatePreviewTargetSet,
	result *FlowRecordCreateFieldsPreviewResult,
	user *models.UserSession,
) (string, error) {
	for _, decision := range flowMeta.Decisions {
		if decision.Name == elementName {
			return e.executeDecision(ctx, interview, &decision, user)
		}
	}
	for _, assignment := range flowMeta.Assignments {
		if assignment.Name == elementName {
			return e.executeAssignment(ctx, interview, &assignment, user)
		}
	}
	for _, lookup := range flowMeta.RecordLookups {
		if lookup.Name == elementName {
			return e.executeRecordLookup(ctx, interview, &lookup, user)
		}
	}
	for _, create := range flowMeta.RecordCreates {
		if create.Name == elementName {
			return e.previewRecordCreate(ctx, interview, &create, targetSet, result, user)
		}
	}
	for _, customErr := range flowMeta.CustomErrors {
		if customErr.Name == elementName {
			return e.executeCustomError(ctx, interview, &customErr)
		}
	}
	for _, update := range flowMeta.RecordUpdates {
		if update.Name == elementName {
			return e.previewRecordUpdate(ctx, interview, &update, user)
		}
	}
	for _, del := range flowMeta.RecordDeletes {
		if del.Name == elementName {
			return "", fmt.Errorf("recordDelete %q was reached before requested recordCreate preview targets", del.Name)
		}
	}
	for _, action := range flowMeta.ActionCalls {
		if action.Name == elementName {
			return "", fmt.Errorf("actionCall %q was reached before requested recordCreate preview targets", action.Name)
		}
	}
	for _, subflow := range flowMeta.Subflows {
		if subflow.Name == elementName {
			return "", fmt.Errorf("subflow %q was reached before requested recordCreate preview targets", subflow.Name)
		}
	}
	for _, loop := range flowMeta.Loops {
		if loop.Name == elementName {
			return "", fmt.Errorf("loop %q was reached before requested recordCreate preview targets", loop.Name)
		}
	}
	return "", apperrors.NewNotFoundError("flow element", elementName)
}

func (e *FlowExecutor) previewRecordCreate(
	ctx context.Context,
	interview *models.FlowInterview,
	create *models.FlowRecordCreate,
	targetSet *recordCreatePreviewTargetSet,
	result *FlowRecordCreateFieldsPreviewResult,
	user *models.UserSession,
) (string, error) {
	record := models.SObject{}
	if err := e.applyRecordCreateFieldMapping(ctx, interview, create, record); err != nil {
		if create.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = err.Error()
			return create.FaultConnector.TargetReference, nil
		}
		return "", err
	}
	for _, input := range create.InputAssignments {
		value, exists := e.resolveValueCtxWithPresence(ctx, interview, user, input.Value)
		if !exists {
			if e.logger != nil {
				e.logger.Debug(fmt.Sprintf(
					"RecordCreate %q preview: input %q is an unresolved elementReference; skipping so default layers can fill it",
					create.Name, input.Field))
			}
			continue
		}
		record[input.Field] = value
	}
	if e.fieldDefaultResolver != nil {
		if err := e.fieldDefaultResolver.Apply(ctx, record, create.Object, interview.ContextData); err != nil {
			if create.FaultConnector != nil {
				interview.ContextData["$Flow.FaultMessage"] = err.Error()
				return create.FaultConnector.TargetReference, nil
			}
			return "", err
		}
	}

	if fields, ok := targetSet.fieldsForObject(create.Object); ok {
		preview := FlowRecordCreatePreview{
			ElementName:   create.Name,
			ObjectAPIName: create.Object,
			Fields:        map[string]interface{}{},
		}
		for fieldAPIName := range fields {
			if value, exists := record[fieldAPIName]; exists {
				preview.Fields[fieldAPIName] = value
			}
		}
		result.RecordCreates = append(result.RecordCreates, preview)
		targetSet.markSeen(create.Object, fields)
	}

	syntheticID := "preview:" + create.Name
	if create.AssignRecordIdToReference != "" {
		interview.ContextData[create.AssignRecordIdToReference] = syntheticID
	}
	if create.StoreOutputAutomatically {
		interview.ContextData[create.Name] = syntheticID
	}
	if create.Connector != nil {
		return create.Connector.TargetReference, nil
	}
	return "", nil
}

func (e *FlowExecutor) previewRecordUpdate(ctx context.Context, interview *models.FlowInterview, update *models.FlowRecordUpdate, user *models.UserSession) (string, error) {
	updates := models.SObject{}
	if err := e.applyRecordUpdateFieldMapping(ctx, interview, update, updates); err != nil {
		if update.FaultConnector != nil {
			interview.ContextData["$Flow.FaultMessage"] = err.Error()
			return update.FaultConnector.TargetReference, nil
		}
		return "", err
	}

	// Preview is read-only. It may pass through the same no-DML field-mapping
	// case that executeRecordUpdate already short-circuits, but it must not
	// implement a second partial interpreter for DML, filters, or assignments.
	if update.FieldMapping == nil ||
		len(update.InputAssignments) > 0 ||
		len(update.Filters) > 0 ||
		len(updates) > 0 ||
		previewRecordUpdateHasCollectionInput(interview, update.InputReference) {
		return "", fmt.Errorf("recordUpdate %q requires DML or assignment semantics before requested recordCreate preview targets", update.Name)
	}

	if update.Connector != nil {
		return update.Connector.TargetReference, nil
	}
	return "", nil
}

func previewRecordUpdateHasCollectionInput(interview *models.FlowInterview, inputReference string) bool {
	if inputReference == "" || interview == nil || interview.ContextData == nil {
		return false
	}
	switch interview.ContextData[inputReference].(type) {
	case []models.SObject, []interface{}:
		return true
	default:
		return false
	}
}
