package plan

import "sort"

type artifactStateEffectSet struct {
	reads  map[string]bool
	writes map[string]bool
}

func newArtifactStateEffectSet() *artifactStateEffectSet {
	return &artifactStateEffectSet{reads: map[string]bool{}, writes: map[string]bool{}}
}

// applyArtifactStateEffects derives the executable read/write contract from
// the compiled AST. This is the artifact authority; no source-text scanner is
// involved, so expression grammar changes cannot silently desynchronize it.
func applyArtifactStateEffects(artifact *Artifact) {
	if artifact == nil {
		return
	}
	sets := make(map[string]*artifactStateEffectSet, len(artifact.Nodes))
	for i := range artifact.Nodes {
		set := newArtifactStateEffectSet()
		sets[artifact.Nodes[i].ID] = set
		collectArtifactNodeEffects(&artifact.Nodes[i], set)
	}
	for i := range artifact.Edges {
		if set := sets[artifact.Edges[i].Source.NodeID]; set != nil {
			collectArtifactBindingsReads(artifact.Edges[i].Bindings, set.reads)
		}
	}
	if set := sets[artifact.Entry.NodeID]; set != nil {
		collectArtifactBindingsReads(artifact.Entry.Bindings, set.reads)
	}
	for i := range artifact.Nodes {
		set := sets[artifact.Nodes[i].ID]
		artifact.Nodes[i].StateEffects = ArtifactStateEffects{
			Reads:  sortedArtifactEffectKeys(set.reads),
			Writes: sortedArtifactEffectKeys(set.writes),
		}
	}
}

func collectArtifactNodeEffects(node *ArtifactNode, set *artifactStateEffectSet) {
	if node == nil || set == nil {
		return
	}
	config := node.Config
	switch node.Opcode {
	case OpcodeSendMessageSent, OpcodeSendMessageCallback:
		if config.SendMessage != nil {
			if config.SendMessage.Target != nil {
				collectArtifactTemplateReads(*config.SendMessage.Target, set.reads)
			}
			if config.SendMessage.Scope != nil {
				collectArtifactExpressionReads(*config.SendMessage.Scope, set.reads)
			}
			if config.SendMessage.Responder != nil {
				collectArtifactExpressionReads(*config.SendMessage.Responder, set.reads)
			}
			if config.SendMessage.SelectOne != nil {
				collectArtifactExpressionReads(*config.SendMessage.SelectOne, set.reads)
			}
			if config.SendMessage.WorkScope != nil {
				collectArtifactExpressionReads(*config.SendMessage.WorkScope, set.reads)
			}
			for _, relatedScope := range config.SendMessage.RelatedScopes {
				collectArtifactExpressionReads(relatedScope, set.reads)
			}
			if config.SendMessage.RelatedScopesArray != nil {
				collectArtifactExpressionReads(*config.SendMessage.RelatedScopesArray, set.reads)
			}
			if config.SendMessage.WorkItems != nil {
				collectArtifactExpressionReads(config.SendMessage.WorkItems.AllowedResponders, set.reads)
			}
			collectArtifactTemplateReads(config.SendMessage.Text, set.reads)
		}
	case OpcodeSetVariable:
		if config.SetVariable != nil {
			for _, assignment := range config.SetVariable.Assignments {
				if assignment.State != "" {
					set.writes[assignment.State] = true
				}
				collectArtifactExpressionReads(assignment.Value, set.reads)
			}
		}
	case OpcodeWait:
		if config.Wait != nil {
			collectArtifactExpressionReads(config.Wait.Until, set.reads)
		}
	case OpcodeRequestApproval:
		if config.RequestApproval != nil {
			collectArtifactTemplateReads(config.RequestApproval.Assignee, set.reads)
			collectArtifactTemplateReads(config.RequestApproval.Summary, set.reads)
			if config.RequestApproval.FeedbackScope != nil {
				collectArtifactExpressionReads(*config.RequestApproval.FeedbackScope, set.reads)
			}
		}
	case OpcodeSwitchFirstMatch, OpcodeSwitchAllMatch:
		if config.Switch != nil {
			for _, switchCase := range config.Switch.Cases {
				if switchCase.Condition != nil {
					collectArtifactExpressionReads(*switchCase.Condition, set.reads)
				}
			}
		}
	case OpcodeJoinAll, OpcodeJoinAny:
		if config.Join != nil {
			collectArtifactBindingsReads(config.Join.Merge, set.reads)
		}
	case OpcodeFanOut:
		if config.FanOut != nil {
			collectArtifactExpressionReads(config.FanOut.Items, set.reads)
		}
	}
}

func collectArtifactTemplateReads(template ArtifactTemplate, reads map[string]bool) {
	for _, segment := range template.Segments {
		if segment.Expression != nil {
			collectArtifactExpressionReads(*segment.Expression, reads)
		}
	}
}

func collectArtifactBindingsReads(bindings []ArtifactBinding, reads map[string]bool) {
	for _, binding := range bindings {
		collectArtifactExpressionReads(binding.Value, reads)
	}
}

func collectArtifactExpressionReads(expression ArtifactExpression, reads map[string]bool) {
	if expression.Op == ArtifactOpLoad && expression.Load != nil && expression.Load.Space == ArtifactLoadRunState && len(expression.Load.Path) != 0 {
		reads[expression.Load.Path[0]] = true
	}
	for _, argument := range expression.Args {
		collectArtifactExpressionReads(argument, reads)
	}
}

func sortedArtifactEffectKeys(values map[string]bool) []string {
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

// compileDocumentStateEffects uses the same expression/template lowering as
// the artifact for validation-time parallel race analysis. Invalid source may
// already have diagnostics; lowering failures simply omit that invalid site's
// effects instead of falling back to a second text grammar.
func compileDocumentStateEffects(v *validator) map[string]ArtifactStateEffects {
	result := make(map[string]ArtifactStateEffects, len(v.document.Graph.Nodes))
	sets := make(map[string]*artifactStateEffectSet, len(v.document.Graph.Nodes))
	for i := range v.document.Graph.Nodes {
		source := &v.document.Graph.Nodes[i]
		set := newArtifactStateEffectSet()
		sets[source.ID] = set
		if !sourceNodeConfigPresent(source) {
			continue
		}
		config, err := lowerArtifactNodeConfig(v, source)
		if err == nil {
			node := ArtifactNode{ID: source.ID, Opcode: mustArtifactOpcode(source), Config: config}
			collectArtifactNodeEffects(&node, set)
		}
	}
	for i := range v.document.Graph.Edges {
		edge := &v.document.Graph.Edges[i]
		set := sets[edge.Source.NodeID]
		source := v.nodes[edge.Source.NodeID]
		target := v.nodes[edge.Target.NodeID]
		if set == nil || source == nil || target == nil {
			continue
		}
		context := referenceContext{sourcePort: edge.Source.Port}
		if source.Type == NodeLoop {
			context.loop = source.Loop
		}
		bindings, err := lowerArtifactBindings(v, edge.Bindings, source, context, v.targetSchema(target, edge.Target.Port))
		if err == nil {
			collectArtifactBindingsReads(bindings, set.reads)
		}
	}
	if entry := v.nodes[v.document.Graph.Entry.NodeID]; entry != nil {
		bindings, err := lowerArtifactBindings(v, v.document.Graph.Entry.Input, &Node{ID: "$entry"}, referenceContext{}, v.targetSchema(entry, v.document.Graph.Entry.Port))
		if err == nil {
			collectArtifactBindingsReads(bindings, sets[entry.ID].reads)
		}
	}
	for nodeID, set := range sets {
		result[nodeID] = ArtifactStateEffects{Reads: sortedArtifactEffectKeys(set.reads), Writes: sortedArtifactEffectKeys(set.writes)}
	}
	return result
}

func sourceNodeConfigPresent(node *Node) bool {
	if node == nil {
		return false
	}
	capability, ok := compilerAuthoringCapabilities.nodeCapability(node.Type)
	return ok && capability.ConfigPresent(node)
}

func compileArtifactBindingReadSet(v *validator, bindings map[string]ValueExpr, source *Node, context referenceContext, target map[string]Schema) map[string]bool {
	reads := map[string]bool{}
	compiled, err := lowerArtifactBindings(v, bindings, source, context, target)
	if err == nil {
		collectArtifactBindingsReads(compiled, reads)
	}
	return reads
}

func mustArtifactOpcode(node *Node) ArtifactOpcode {
	opcode, _ := artifactOpcode(node)
	return opcode
}
